Compare commits

...

412 Commits
0.22 ... 0.75

Author SHA1 Message Date
Pascal Vizeli
6c75957578 Fix version merge conflict 2017-11-24 22:18:13 +01:00
Pascal Vizeli
3a8307acfe Fix panel (#258) 2017-11-24 21:54:56 +01:00
Pascal Vizeli
f20c7d42ee Update home-assistant to version 0.58.1 2017-11-24 14:13:16 +01:00
Pascal Vizeli
9419fbff94 Add new pannel (#257) 2017-11-24 14:01:17 +01:00
Pascal Vizeli
3ac6c03637 Update Hass.io to 0.75 2017-11-22 16:46:14 +01:00
Pascal Vizeli
a95274f1b3 Use tini for home-assistant docker (#255) 2017-11-21 15:55:10 +01:00
Markus
9d2fb87cec typo (#254) 2017-11-20 13:48:04 +01:00
Paulus Schoutsen
ce9c3565b6 Hassio panel split (#249)
* Allow serving two types of panels

* Remove old panel

* Make backwards compatible

* Add comment
2017-11-20 13:44:04 +01:00
Pascal Vizeli
b0ec58ed1b Update Home-Assistant to 0.57.3 2017-11-13 20:47:36 +01:00
Pascal Vizeli
893a5f8dd3 Update Home-Assistant to 0.57.3 2017-11-13 18:44:12 +01:00
Pascal Vizeli
98064f6a90 Update Home-Assistant to 0.57.2 2017-11-06 11:25:25 +01:00
Pascal Vizeli
5146f89354 Update Home-Assistant to 0.57.2 2017-11-06 07:24:04 +01:00
Pascal Vizeli
fb46592d48 Pump version to 0.75 2017-11-05 00:35:04 +01:00
Pascal Vizeli
b4fb5ac681 Fix merge conflict 2017-11-05 00:32:17 +01:00
Pascal Vizeli
4b7201dc59 Update Home-Assistant to 0.57.1 2017-11-05 00:13:11 +01:00
Pascal Vizeli
3a5a4e4c27 Update hass.io to 0.74 2017-11-05 00:12:49 +01:00
Pascal Vizeli
70104a9280 Set api token for access requirements (#238)
* Set api token for access requirements

* fix uuid

* make robust

* fix names
2017-11-05 00:07:49 +01:00
Pascal Vizeli
efbc7b17a1 Use init system for add-ons (#237)
* Use init system for add-ons

* Update const.py

* Update validate.py

* Update addon.py

* Update addon.py

* remove options

* remove options p2

* remove options p3

* Update addon.py
2017-11-04 21:52:41 +01:00
Pascal Vizeli
64c5e20fc4 Update Home-Assistant to 0.57 2017-11-04 11:43:04 +01:00
Pascal Vizeli
13498afa97 Update Home-Assistant to 0.57 2017-11-04 11:33:55 +01:00
Pascal Vizeli
f6375f1bd6 Pump version to 0.74 2017-10-25 12:12:13 +02:00
pvizeli
8fd1599173 Fix merge conflict version 2017-10-25 12:09:33 +02:00
Pascal Vizeli
63302b73b0 Update hass.io to 0.73 2017-10-25 12:01:27 +02:00
Pascal Vizeli
f591f67a2a Show hardware GPIO interface (#233)
* Update hardware.py

* Update host.py

* Update API.md

* Update API.md

* fix lint
2017-10-25 11:50:00 +02:00
Pascal Vizeli
cda3184a55 Add support for legacy mode (#232)
* Add support for legacy mode

* Update const.py

* add legacy mode

* Update addon.py

* Update addon.py

* Update addon.py

* Update addon.py
2017-10-24 17:23:33 +02:00
Pascal Vizeli
afc811e975 Update Home-Assistant to 0.56.2 2017-10-24 06:59:19 +02:00
Pascal Vizeli
2e169dcb42 Update Home-Assistant to 0.56.2 2017-10-24 00:08:57 +02:00
Pascal Vizeli
34e24e184f Update Home-Assistant to 0.56.1 2017-10-23 00:26:49 +02:00
Pascal Vizeli
2e4751ed7d Update version.json 2017-10-23 00:16:38 +02:00
Pascal Vizeli
8c82c467d4 Fix aiohttp 2.3.1 (#231) 2017-10-22 14:05:45 +02:00
Pascal Vizeli
f3f6771534 Add a static entry for hassio api (#230) 2017-10-22 13:53:41 +02:00
Pascal Vizeli
0a75a4dcbc Update Home-Assistant to 0.56 2017-10-22 11:16:51 +02:00
Pascal Vizeli
1a4542fc4e Update Home-Assistant to 0.56 2017-10-22 10:34:31 +02:00
Pascal Vizeli
7e0525749e Pump version to 0.73 2017-10-17 22:49:35 +02:00
Pascal Vizeli
b33b26018d fix version conflict 2017-10-17 22:47:20 +02:00
Pascal Vizeli
66c93e7176 Minimize downtime to 1 sec (#223) 2017-10-17 22:25:29 +02:00
Pascal Vizeli
5674d32bad Update hass.io version 0.72 2017-10-17 21:54:10 +02:00
Pascal Vizeli
7a84972770 Better close/loop handling (#221)
* Better close/loop handling

* Update bootstrap.py

* Update __main__.py

* Update core.py

* Update __main__.py

* Update __main__.py

* Update supervisor.py

* Update supervisor.py

* Update const.py

* fix lint
2017-10-17 16:04:37 +02:00
Pascal Vizeli
638f0f5371 Update Home-Assistant to 0.55.2 2017-10-17 00:07:42 +02:00
Pascal Vizeli
dca1b6f1d3 Update Home-Assistant to 0.55.2 2017-10-17 00:02:17 +02:00
Pascal Vizeli
2b0ee109d6 Rollback Home-Assistant 0.55.1 2017-10-16 19:06:17 +02:00
Pascal Vizeli
e7430d87d7 Update docker image validate (#220) 2017-10-16 17:15:09 +02:00
Pascal Vizeli
9751c1de79 Update Home-Assistant to 0.55.1 2017-10-16 15:50:48 +02:00
Pascal Vizeli
c497167b64 Update Home-Assistant to 0.55.1 2017-10-16 15:50:08 +02:00
Pascal Vizeli
7fb2aca88b Pump version to 0.72 2017-10-13 22:28:15 +02:00
Pascal Vizeli
0d544845b1 Update hass.io to version 0.71 2017-10-13 22:14:11 +02:00
Pascal Vizeli
602eb472f9 Allow to set a option als optional (#218)
* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* fix bug

* Extend schema

* Update validate.py

* fix lint

* Update validate.py

* Update validate.py

* Fix deepmerge

* Update setup.py

* Update validate.py
2017-10-13 22:02:41 +02:00
Pascal Vizeli
f22fa46bdb Pump version to 0.71 2017-10-10 07:24:35 +02:00
Pascal Vizeli
4171a28260 Update Hass.io to 0.70 2017-10-10 07:10:01 +02:00
Pascal Vizeli
55365a631a Bugfix weburl (#217) 2017-10-10 07:08:56 +02:00
Pascal Vizeli
547415b30b Pump version to 0.70 2017-10-09 15:38:49 +02:00
pvizeli
cbf79f1fab Fix version merge conflict 2017-10-09 15:25:30 +02:00
Pascal Vizeli
31cc1dce82 Update Hass.io to version 0.69 2017-10-09 15:15:30 +02:00
Pascal Vizeli
8a11e6c845 Check if a option is missing inside nested lists (#216)
* Update validate.py

* fix lint
2017-10-09 14:08:29 +02:00
Pascal Vizeli
2df4f80aa5 More log output (#214)
* Update snapshot.py

* Update __init__.py

* Update snapshot.py

* Update snapshot.py

* fix lint
2017-10-09 13:30:15 +02:00
Pascal Vizeli
68566ee9e1 Update homeassistant.py (#215) 2017-10-09 13:21:21 +02:00
Pascal Vizeli
fe04b7ec59 Remove dedicated API calls (#212)
* Update addons.py

* Update API.md

* Update addon.py

* Update addon.py

* Update addons.py
2017-10-09 10:48:17 +02:00
Pascal Vizeli
38f96d7ddd Remove unknown options from input (#213)
* Update validate.py

* Update validate.py

* Cleanup unneeded code
2017-10-09 10:09:43 +02:00
Pascal Vizeli
2b2edd6e98 Update Home-Assistant to version 0.55 2017-10-08 09:49:06 +02:00
Pascal Vizeli
361969aca2 Update Home-Assistant to version 0.55 2017-10-08 09:43:07 +02:00
Pascal Vizeli
e61e7f41f2 Pump version to 0.69 2017-10-03 17:37:48 +02:00
Pascal Vizeli
75150fd149 Update hass.io to 0.68 2017-10-03 17:12:40 +02:00
Pascal Vizeli
bd1c8be1e1 Support new API for add-on STDIN support (#207)
* Add function to write data to add-on stdin with API

* Update API Doc

* Add to api

* Update addon.py
2017-10-03 16:44:48 +02:00
Pascal Vizeli
f167197640 New config for homeassistant_api & upgrade snapshot (#206)
* New config for homeassistant_api & upgrade snapshot

* fix lint
2017-10-03 11:47:35 +02:00
Pascal Vizeli
f084ecc007 Pump version to 0.68 2017-10-03 00:59:12 +02:00
Pascal Vizeli
65becbd0ae Update hass.io to 0.67 2017-10-03 00:41:30 +02:00
Pascal Vizeli
f38e28a4d9 Add interface and home-assistant api proxy (#205)
* Add initial for hass interface

* For better compatibility, remove extra options for cleanup old stuff

* Add new functions to api

* Add api proxy to home-assistant

* use const

* fix lint

* fix lint

* Add check_api_state function

* Add api watchdog

* Fix lint

* update output

* fix url

* Fix API call

* fix API documentation

* remove password

* fix api call to hass api only

* fix problem with config missmatch

* test

* Detect wrong ssl settings

* disable watchdog & add options

* Update API
2017-10-03 00:31:14 +02:00
Pascal Vizeli
2998cd94ff Allow dynamic handling of proto part (#203)
* Allow dynamic handling of proto part

* Fix lint

* fix bug
2017-10-01 00:03:06 +02:00
Pascal Vizeli
79e2f3e8ab Pump version to 0.67 2017-09-30 12:09:22 +02:00
Pascal Vizeli
13291f52f2 Update hass.io to 0.66 2017-09-30 12:03:37 +02:00
Pascal Vizeli
4baa80c3de Map sysfs devices/platform/soc data 2017-09-30 11:04:27 +02:00
Pascal Vizeli
be28a6b012 fix spell 2017-09-29 23:31:07 +02:00
Pascal Vizeli
d94ada6216 Pump version to 0.65 2017-09-29 22:12:52 +02:00
Pascal Vizeli
b2d7743e06 Update hass.io to 0.65 2017-09-29 21:56:15 +02:00
Pascal Vizeli
40324beb72 Add support for kernel gpio interface (#202)
* Add support for kernel gpio interface

* Update addon.py

* fix git python module change

* Update git.py
2017-09-29 21:42:33 +02:00
Pascal Vizeli
c02f6913b3 Extend label schema (#200)
* Update build.py

* Update build.py

* fix lint
2017-09-29 16:29:23 +02:00
Pascal Vizeli
d56af22d5e Dockerfiles for new build system 2017-09-27 17:17:08 +02:00
Pascal Vizeli
1795103086 Pump version to 0.65 2017-09-26 09:10:56 +02:00
pvizeli
02e1689dd1 Fix version confict 2017-09-26 08:48:31 +02:00
Pascal Vizeli
ab4d96331f Update Home-Assistant to 0.54 2017-09-23 12:03:08 +02:00
Pascal Vizeli
cb881cba28 Update Home-Assistant to 0.54 2017-09-23 12:02:49 +02:00
Pascal Vizeli
44b247f397 Update hass.io to 0.64 2017-09-19 22:06:48 +02:00
Pascal Vizeli
8bb43daf91 Remove support for custom configs / configs for other hass.io versions (#197)
* Remove support for custom configs

* not need since supervisor is autoupdate
2017-09-19 21:52:18 +02:00
Pascal Vizeli
a7e65613d6 Update validate.py (#196) 2017-09-19 20:43:44 +02:00
Pascal Vizeli
3c04c71401 Update build system to origin docker (#191)
* Update build system to origin docker

* Rename build env

* fix lint p1

* fix bug & add more log info for snapshot/restore

* fix exception

* Log build info

* revert last change

* fix regex
2017-09-19 18:06:34 +02:00
Pascal Vizeli
1353d52bd1 Reset json file to default on schema error (#193) 2017-09-19 17:51:16 +02:00
Pascal Vizeli
7701457791 Update resinos to 1.1 2017-09-18 22:05:46 +02:00
Pascal Vizeli
b7820bc6a6 Update resinos to 1.1 2017-09-18 22:05:11 +02:00
Pascal Vizeli
df66102de0 Pump version to 0.64 2017-09-17 14:42:35 +02:00
Pascal Vizeli
4b308d0de1 Fix version conflict 2017-09-17 14:40:20 +02:00
Pascal Vizeli
4448ba886b Update hass.io to version 0.63 2017-09-17 14:25:02 +02:00
Pascal Vizeli
f39006be01 Change flow and start landingpage faster (#189)
* Change flow and start landingpage faster

* run homeassistant after install

* Update homeassistant.py
2017-09-16 14:45:36 +02:00
Pascal Vizeli
e5204eef8a Update Home-Assistant to 0.53.1 2017-09-14 01:28:01 +02:00
Pascal Vizeli
1f07d47fd6 Update Home-Assistant to 0.53.1 2017-09-14 01:27:36 +02:00
Pascal Vizeli
ba352abf0b Pump version to 0.63 2017-09-12 20:13:41 +02:00
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
Pascal Vizeli
f798e75e30 update hassio to 0.41 2017-07-07 00:22:30 +02:00
Pascal Vizeli
710f8570d2 Update tasks.py 2017-07-07 00:20:38 +02:00
Pascal Vizeli
4dfd11ffb4 Update const.py 2017-07-07 00:00:45 +02:00
Pascal Vizeli
4e4368debb Update setup.py 2017-07-06 23:49:42 +02:00
Pascal Vizeli
30c7ddf4ef Update version.json 2017-07-06 23:48:03 +02:00
Pascal Vizeli
7186f5a8c0 Add more addons functions. (#91)
* Add more addons functions.

* fix lint

* fix lint p2

* Allow more customable network settings

* fix lint

* change point of validate

* fix lint

* fix handling

* fix lint & validate data before write
2017-07-06 23:40:49 +02:00
Pascal Vizeli
f52d1c4509 Pump version 2017-07-06 01:52:30 +02:00
Pascal Vizeli
4dbece8e8e Merge remote-tracking branch 'origin/dev' 2017-07-06 01:42:07 +02:00
Pascal Vizeli
f731c630a6 Update version.json 2017-07-06 01:34:37 +02:00
Pascal Vizeli
0ac96c207e Convert homeassistant into a dict (snapshot) (#90)
* Convert homeassistant into a dict

* fix lint

* fix bugs

* cleanup code

* fix cleanup
2017-07-06 01:29:09 +02:00
Pascal Vizeli
e2a29b7290 Add snapshot feature (#88)
* Add API layout for snapshot

* Update api

* Add support for export/import docker images

* Move restore into addon

* Add restore to addon

* Fix lint

* fix lint

* cleanup

* init object

* fix executor

* cleanup

* Change flow of init

* Revert "Change flow of init"

This reverts commit 6b3215e44c.

* allow restore from none

* forward working

* add size

* add context for snapshot

* add init function to set meta data

* update local addon on load

* add more validate and optimaze code

* Optimaze code for restore data

* add validate layer

* Add more function to snapshot / cleanup others

* finish snapshot function

* Cleanup config / optimaze code

* Finish snapshot on core

* Some improvments first object for api

* finish

* fix lint p1

* fix lint p2

* fix lint p3

* fix async with

* fix lint p4

* fix lint p5

* fix p6

* make staticmethod

* fix schema

* fix parse system data

* fix bugs

* fix get function

* extend snapshot/restore

* add type

* fix lint

* move to gz / xz is to slow

* move to gz / xz is to slow p2

* Fix config folder

* small compresslevel for more speed

* fix lint

* fix load

* fix tar stream

* fix tar stream p2

* fix parse

* fix partial

* fix start hass

* fix rep

* fix set

* fix real

* fix generator

* Cleanup old image

* add log

* fix lint

* fix lint p2

* fix load from tar
2017-07-05 18:16:14 +02:00
Pascal Vizeli
f107a73e28 Update HomeAssistant to 0.48.1 2017-07-05 11:16:19 +02:00
Pascal Vizeli
2c68e5801f Update HomeAssistant to 0.48.1 2017-07-05 11:15:57 +02:00
Pascal Vizeli
91502a0727 Update HomeAssistant 0.48 2017-07-03 07:03:35 +02:00
Pascal Vizeli
872f1d0ae3 update homeassistant 0.48 2017-07-03 07:03:03 +02:00
Pascal Vizeli
3c4240a8a8 Update __init__.py 2017-06-29 21:10:42 +02:00
Pascal Vizeli
7a470bb3ac Pump version 0.39 2017-06-29 12:03:00 +02:00
pvizeli
766a9af54e Update version after merge 2017-06-29 12:00:39 +02:00
Pascal Vizeli
ca303a62f2 UI update for 0.38 (#87)
* Update UI

* Update frontend
2017-06-29 10:24:21 +02:00
Pascal Vizeli
90030d3a28 Add websession for reload (#86) 2017-06-29 07:57:04 +02:00
Pascal Vizeli
0ed48a7741 fix self update style on startup (#85)
* fix self update style on startup

* Check beta/dev upstream on update function
2017-06-29 07:50:42 +02:00
Pascal Vizeli
a33d765776 Update Hass.IO dev 2017-06-29 07:23:32 +02:00
Pascal Vizeli
6bb4f0e369 Update API.md 2017-06-29 00:03:23 +02:00
Pascal Vizeli
56a9f64730 Fix write options handling (#84)
* Fix write options handling

* Fix bug
2017-06-28 23:06:06 +02:00
Pascal Vizeli
d5eb66bc0d Use tmp folder / fix log bug / add executor (#83)
* Use tmp folder / fix log bug / add executor

* Update __main__.py

* Update .travis.yml

* Add autoupdate on startup.

* Fix bug

* Move selfupdate code part into start
2017-06-28 16:22:44 +02:00
Pascal Vizeli
40343089b5 Fix error handing (#82)
Add an optional extended description…
2017-06-26 09:17:09 +02:00
Pascal Vizeli
1b887e38d6 Add new data Layer for addons (#81)
* Add new data Layer for addons

* draft v2

* part 3

* Cleanups for new addon layout

* cleanup part 5

* fix lint p1

* fix lint p2

* fix api bug

* Fix lint p3

* fix lint p4

* fix lint p5

* fix lint p6

* fix update

* fix lint

* Update repo add code part 1

* new repository load code

* reorder code

* Code cleanup p1

* Don't allow change options for not installed addons

* Cleanup error handling

* Fix addon restart config bug

* minimize timeout for bad addons

* set core timeout for docker to 15

* fix lint

* fix start bug

* Add startuptype

* change names / fix update bug

* fix update bug p2

* Cleanup arch

* Ignore built-in repositories

* fix lint

* fix bug

* fix bug p4

* fix arch handling / better bugfix for boot option

* fix lint

* fix arch

* fix options

* fix close is no coro

* update api description & fix lint

* Fix error

* fix api validate config

* cleanup api

* Fix repo loader

* cleanup slugs

* fix lint
2017-06-23 00:51:28 +02:00
Pascal Vizeli
ba96f99cde Update HomeAssistant 0.47.1 2017-06-21 22:45:36 +02:00
Pascal Vizeli
b7f5cc868b HomeAssistant 0.47.1 2017-06-21 22:18:46 +02:00
Pascal Vizeli
c8343fdfb0 Update HomeAssistant 0.47.1 2017-06-21 22:16:44 +02:00
Pascal Vizeli
91e4bf1676 Update HomeAssistant 2017-06-18 00:46:54 +02:00
Pascal Vizeli
6dba8d4ef9 Update HomeAssistant 2017-06-18 00:40:13 +02:00
Pascal Vizeli
65eaed4f90 Pump version 2017-06-17 23:58:07 +02:00
Pascal Vizeli
8233083392 Merge remote-tracking branch 'origin/dev' 2017-06-17 23:55:24 +02:00
Pascal Vizeli
106378d1d0 Update version.json 2017-06-17 23:47:02 +02:00
Pascal Vizeli
01d18d5ff3 Allow custom options without validate (#80) 2017-06-17 22:47:56 +02:00
Pascal Vizeli
6d23f3bd1c Use our new base image 2017-06-14 16:55:03 +02:00
Pascal Vizeli
ef96579a29 Update HomeAssistant 0.46.1 2017-06-10 00:55:17 +02:00
Pascal Vizeli
44f0a9f21a Update HomeAssistant 0.46.1 2017-06-10 00:54:49 +02:00
Pascal Vizeli
d854307acb Remove container before delete images (#78) 2017-06-08 17:11:14 +02:00
Pascal Vizeli
334b41de71 Pump version to 0.37 2017-06-05 13:02:13 +02:00
Pascal Vizeli
1da50eab7a Fix versions 2017-06-05 12:59:44 +02:00
Pascal Vizeli
b119a42f4d Update Hass.IO 0.36 2017-06-05 12:56:14 +02:00
Pascal Vizeli
99aa438817 Allow extend cap for addons (#77)
* Allow extend cap for addons

* cleanup
2017-06-05 12:46:45 +02:00
Pascal Vizeli
99fa91f480 Add better validator for device 2017-06-05 12:45:43 +02:00
Pascal Vizeli
93969d264d Update HomeAssistant 0.46 2017-06-04 09:32:55 +02:00
Pascal Vizeli
711e199977 Update HomeAssistant 0.46 2017-06-04 09:11:46 +02:00
Pascal Vizeli
4e645332c3 Pump version to 0.36 2017-06-03 00:15:46 +02:00
Pascal Vizeli
df8afb3337 Merge pull request #76 from home-assistant/dev
Release 0.35
2017-06-03 00:01:16 +02:00
Pascal Vizeli
255a33fc08 Update Hass.IO to 0.35 2017-06-02 23:52:48 +02:00
Pascal Vizeli
d15b6f0294 Allow map special device to homeassistant docker (#75)
* Allow map special device to homeassistant docker

* fix lint

* fix '/dev/'
2017-06-02 23:39:54 +02:00
Pascal Vizeli
1aa24e40ae Update README.md 2017-06-02 10:59:55 +02:00
Pascal Vizeli
c0bde4a488 Next version 2017-06-01 00:39:39 +02:00
Pascal Vizeli
2a09b70294 Merge pull request #74 from home-assistant/dev
Release 0.34
2017-06-01 00:01:48 +02:00
Pascal Vizeli
e35b0a54c1 Pump version to 0.34 2017-05-31 23:53:22 +02:00
Pascal Vizeli
8287330c67 cleanup 2017-05-31 23:44:05 +02:00
Pascal Vizeli
6b16da93cd WIP: Refactory / Cleanup docker base (#73)
* Refactory / Cleanup docker base

* Check ID of running image

* Small bugs / lint

* Add log info

* Fix lint

* Add a real cleanup solution

* fix unused import

* Cleanup restart after updates

* Use restart callback

* rename callback

* Add info log for cleanup & fix lint

* Fix lint

* fix wrong id

* fix set addon as install
2017-05-31 23:41:04 +02:00
bestlibre
c1cd9bba45 Adding tmpfs to addon config (#72)
* Adding tmpfs to addon config

* Adding vol.Match and correcting syntax error

* Missing import and linting

* Update addon.py

* Revert "Update addon.py"

This reverts commit 82798c8f2d.

* optimaze code
2017-05-31 09:39:22 +02:00
Pascal Vizeli
e33420f26e Pump version to 0.34 2017-05-24 00:09:47 +02:00
Pascal Vizeli
abd9683e11 Fix merge conflicts 2017-05-24 00:07:49 +02:00
Pascal Vizeli
8cbeabbe21 Pump Hass.IO version 2017-05-23 23:58:20 +02:00
Pascal Vizeli
df7d988d2f Try to fetch timezone on startup (#70) 2017-05-23 23:17:20 +02:00
Pascal Vizeli
544c009b9c Fix rewrite config to addon on restart (#71) 2017-05-23 22:52:28 +02:00
Pascal Vizeli
b2e0babc60 Update HomeAssistant to 0.45.1 2017-05-23 00:04:55 +02:00
Pascal Vizeli
f7c79cbd3a Update HomeAssistant to 0.45.1 2017-05-22 23:55:47 +02:00
Pascal Vizeli
587e9618da Pump version 2017-05-22 23:22:06 +02:00
Pascal Vizeli
cb2dd3b81c Fix 2017-05-22 23:17:02 +02:00
Pascal Vizeli
8d4dd7de3f Update version.json 2017-05-22 23:07:23 +02:00
Pascal Vizeli
6927c989d0 Timezone (#69)
* Add Timezone support

* finish PR

* fix lint
2017-05-22 22:56:44 +02:00
Pascal Vizeli
97853d1691 Add support for hostname change. (#68)
* Add support for hostname change.

* Fix lint

* Fix lint p2

* Update network.py
2017-05-22 21:59:19 +02:00
Pascal Vizeli
0cdef0d118 Allow add-on to run on host network (#67)
* Allow add-on to run on host network

* cleanup name

* fix lint
2017-05-22 21:52:37 +02:00
Justin Weberg
0b17ffc243 Update readme (#66)
Update installation section for appearance and ease of understanding.
2017-05-21 17:13:36 -07:00
Pascal Vizeli
c516d46f16 Update HomeAssistant 0.45 2017-05-21 08:18:48 +02:00
Pascal Vizeli
cb8ec22b6d Update HomeAssistant 0.45 2017-05-21 08:13:51 +02:00
Pascal Vizeli
4a5fbd79c1 Update ResinOS v0.8 2017-05-20 16:38:09 +02:00
Pascal Vizeli
b636a03567 Update ResinOS v0.8 2017-05-20 16:37:32 +02:00
Pascal Vizeli
c96faf7c0a Pump version 0.32 2017-05-20 01:13:28 +02:00
Pascal Vizeli
2e1cd4076a Merge pull request #64 from home-assistant/dev
Release 0.31
2017-05-20 00:21:40 +02:00
Pascal Vizeli
9984a638ba fix lint 2017-05-20 00:19:48 +02:00
Pascal Vizeli
a492bccc03 Update Hass.IO 0.31 2017-05-20 00:16:50 +02:00
Pascal Vizeli
e7a0e0f565 update pannel 2017-05-20 00:08:11 +02:00
Pascal Vizeli
030e081d45 Update frontend (#63) 2017-05-19 23:59:25 +02:00
Pascal Vizeli
8537536368 Delete hassio-main.html.gz 2017-05-19 23:47:55 +02:00
Pascal Vizeli
f03f323aac Add files via upload 2017-05-19 23:47:34 +02:00
Pascal Vizeli
58c0c67796 Update __init__.py 2017-05-19 23:26:41 +02:00
Pascal Vizeli
f5e196a663 wrap panel into function 2017-05-19 23:24:02 +02:00
Pascal Vizeli
808df68e57 fix panel v2 2017-05-19 23:18:09 +02:00
Pascal Vizeli
fa51c2e6e9 use pathlib 2017-05-19 22:51:46 +02:00
Pascal Vizeli
ba3760e770 Revert API change 2017-05-19 22:48:17 +02:00
Pascal Vizeli
ad1a8557b8 Fix panel & new startup type (#62)
* Fix pannel

* Add new startup  type
2017-05-19 22:31:34 +02:00
Pascal Vizeli
fe91f812d9 Add hostname support for hc protocol 2017-05-19 09:56:30 +02:00
Pascal Vizeli
4cc11305c7 Pump version to 0.31 2017-05-19 09:47:25 +02:00
Pascal Vizeli
898c0330c8 Merge pull request #61 from home-assistant/dev
Release 0.30
2017-05-18 18:14:44 +02:00
Pascal Vizeli
33e5f94f1f cleanup 2017-05-18 18:10:34 +02:00
Pascal Vizeli
da4ee63890 Update version.json 2017-05-18 18:08:29 +02:00
Pascal Vizeli
d34203b133 Remove mnt mount (#60)
* Remove mnt mount

* fix lint
2017-05-18 17:45:44 +02:00
Pascal Vizeli
23addfb9a6 Pump version 2017-05-18 17:40:02 +02:00
Pascal Vizeli
81e1227a7b Merge pull request #59 from home-assistant/dev
Release 0.29
2017-05-17 23:43:09 +02:00
Pascal Vizeli
75be8666a6 Update version.json 2017-05-17 23:41:50 +02:00
Pascal Vizeli
6031a60084 Add addon share and allow to mount host mnt (#58)
* Add addon share and allow to mount host mnt

* fix comments logs
2017-05-17 17:21:54 +02:00
Pascal Vizeli
39d5785118 Allow config.json to manipulate docker env. (#57)
Allow config.json to manipulate docker env.
2017-05-17 14:45:02 +02:00
Pascal Vizeli
bddcdcadb2 Pump version 2017-05-17 14:25:57 +02:00
Pascal Vizeli
3eac6a3366 Merge pull request #56 from home-assistant/dev
Release 0.28
2017-05-16 22:31:47 +02:00
Pascal Vizeli
3c7b962cf9 update hass.io 2017-05-16 22:17:19 +02:00
Pascal Vizeli
bd756e2a9c fix dev regex 2017-05-16 22:11:42 +02:00
Pascal Vizeli
e7920bee2a Fix group regex 2017-05-16 22:03:51 +02:00
Pascal Vizeli
ebcc21370e Add policy for mappings (#55)
* Add policy for mappings

* fix travis
2017-05-16 17:15:35 +02:00
Pascal Vizeli
34c4acf199 Add device support (#54)
Add device support
2017-05-16 14:50:47 +02:00
Pascal Vizeli
47e45dfc9f Pump version 2017-05-16 11:45:20 +02:00
Pascal Vizeli
2ecea7c1b4 Merge pull request #53 from home-assistant/dev
Release 0.27
2017-05-16 00:20:29 +02:00
Pascal Vizeli
5c0eccd12f Bugfix attach container/image (#52) 2017-05-16 00:07:43 +02:00
Pascal Vizeli
f34ab9402b Fix remove (#51) 2017-05-15 23:39:34 +02:00
Pascal Vizeli
2569a82caf Update Hass.IO version 2017-05-15 23:27:18 +02:00
Pascal Vizeli
4bdd256000 Use label instead env, cleanup build (#50)
* Use label instead env, cleanup build

* Update const.py

* fix lint

* add space

* fix lint

* use dynamic type

* fix lint

* fix path

* fix label read

* fix bug
2017-05-15 23:19:35 +02:00
Pascal Vizeli
6f4f6338c5 Pump version 2017-05-15 16:35:30 +02:00
Pascal Vizeli
7cb72b55a8 Merge pull request #49 from home-assistant/dev
Release 0.26
2017-05-15 00:26:30 +02:00
Pascal Vizeli
1a9a08cbfb Update version.json 2017-05-15 00:17:59 +02:00
Pascal Vizeli
237ee0363d Update error message (#48) 2017-05-15 00:08:15 +02:00
Pascal Vizeli
86180ddc34 Allow every repository to make a local build (#47)
* Allow every repository to make a local build

* store version of build

* cleanup code

* fix lint
2017-05-14 23:32:54 +02:00
Pascal Vizeli
eed41d30ec Update const.py 2017-05-13 23:17:23 +02:00
Pascal Vizeli
0b0fd6b910 Add files via upload 2017-05-13 19:14:54 +02:00
Pascal Vizeli
1f887b47ab register panel on core 2017-05-13 17:44:16 +02:00
Pascal Vizeli
affd8057ca WIP Add panel to hass.io (#46)
* Add poliymare

* Add commit

* add static route

* fix name

* add panel
2017-05-13 17:41:46 +02:00
Pascal Vizeli
7a8ee2c46a Merge pull request #45 from home-assistant/dev
Release 0.25
2017-05-12 16:20:12 +02:00
Pascal Vizeli
35fe1f464c Update Hass.IO 0.25 2017-05-12 16:15:18 +02:00
Pascal Vizeli
0955bafebd Update data handling of addons (#44)
* Update data handling of addons

* Update addons api

* Update data.py

* Update data.py

* Add url fix bug
2017-05-12 16:14:49 +02:00
Pascal Vizeli
2e0c540c63 Pump version 2017-05-12 08:53:20 +02:00
Pascal Vizeli
6e9ef17a28 Merge pull request #43 from home-assistant/dev
Release 0.24
2017-05-12 01:47:48 +02:00
Pascal Vizeli
eb3cdbfeb9 update Hass.IO 0.24 2017-05-12 01:37:42 +02:00
Pascal Vizeli
f4cb16ad09 WIP: Add support for build docker on local repository (#42)
* Add support for build docker on local repository

* Add docker support

* finish build

* change api

* add dockerfile generator

* finish it

* fix lint

* fix path

* fix path

* fix copy

* add debug stuff

* fix docker template

* cleanups

* fix addons

* change handling

* fix lint / cleanup code

* fix lint

* tag
2017-05-12 01:37:03 +02:00
Pascal Vizeli
956af2bd62 Add security api and TOTP on supervisor (#41)
* Add security api and TOTP on supervisor

* finish security api

* fix lint

* fix lint p2

* add new api view to init

* Task session cleanup / fix hass wachdog

* fix lint

* fix api return

* fix check
2017-05-10 22:02:47 +02:00
Pascal Vizeli
b76cd5c004 Add files via upload 2017-05-10 17:01:35 +02:00
Pascal Vizeli
61d9301dcc Add files via upload 2017-05-10 11:06:34 +02:00
Pascal Vizeli
2ded05be83 Add files via upload 2017-05-10 10:39:00 +02:00
Pascal Vizeli
899d6766c5 Pump version to 0.24 2017-05-10 00:30:11 +02:00
Pascal Vizeli
c67d57cef4 Update version.json 2017-05-10 00:15:59 +02:00
Pascal Vizeli
b5cca7d341 Update __init__.py 2017-05-10 00:10:11 +02:00
Pascal Vizeli
8919f13911 Fix api (#40)
* Bugfix api call

* add log
2017-05-10 00:00:30 +02:00
Pascal Vizeli
990ae49608 Add tasks and watchdog for homeassistant (#39)
* Add tasks and watchdog for homeassistant

* code cleanup
2017-05-09 23:08:15 +02:00
Pascal Vizeli
c2ba02722c Add arch to addon config / hole api (#38)
* Add arch to addon config / hole api

* fix wrong copy past
2017-05-09 17:03:59 +02:00
Pascal Vizeli
5bd1957337 Update README.md 2017-05-09 09:06:48 +02:00
Pascal Vizeli
f59f0793bc Add restart support to homeassistant and add-ons (#37) 2017-05-08 23:31:30 +02:00
Pascal Vizeli
63b96700e0 Add more infos to /addons/xy/info (#36) 2017-05-08 19:46:08 +02:00
Pascal Vizeli
dffbcc2c7e Pump version to 0.23 2017-05-08 13:00:57 +02:00
Pascal Vizeli
0dbe1ecc2a Merge remote-tracking branch 'origin/master' into dev 2017-05-08 12:58:59 +02:00
Pascal Vizeli
da8526fcec Update Version 0.44.2 - 0.22 2017-05-08 12:00:47 +02:00
Pascal Vizeli
933b6f4d1e Update hass to 0.44.2 2017-05-08 07:11:35 +02:00
Pascal Vizeli
16f2dfeebd Revert 0.44.1 2017-05-08 06:59:53 +02:00
59 changed files with 5629 additions and 1464 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
# General files
.git
.github
# Test related files
.tox
# Temporary files
**/__pycache__

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "home-assistant-polymer"]
path = home-assistant-polymer
url = https://github.com/home-assistant/home-assistant-polymer

View File

@@ -2,7 +2,7 @@ sudo: false
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: "3.5" - python: "3.6"
cache: cache:
directories: directories:

389
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: On error:
```json ```json
{ {
"result": "error", "result": "error",
@@ -12,7 +13,8 @@ On error:
} }
``` ```
On success On success:
```json ```json
{ {
"result": "ok", "result": "ok",
@@ -20,10 +22,9 @@ On success
} }
``` ```
### HassIO ### Hass.io
- GET `/supervisor/ping` - GET `/supervisor/ping`
- GET `/supervisor/info` - GET `/supervisor/info`
The addons from `addons` are only installed one. The addons from `addons` are only installed one.
@@ -32,16 +33,19 @@ The addons from `addons` are only installed one.
{ {
"version": "INSTALL_VERSION", "version": "INSTALL_VERSION",
"last_version": "LAST_VERSION", "last_version": "LAST_VERSION",
"arch": "armhf|aarch64|i386|amd64",
"beta_channel": "true|false", "beta_channel": "true|false",
"timezone": "TIMEZONE",
"addons": [ "addons": [
{ {
"name": "xy bla", "name": "xy bla",
"slug": "xy", "slug": "xy",
"description": "description",
"repository": "12345678|null", "repository": "12345678|null",
"version": "LAST_VERSION", "version": "LAST_VERSION",
"installed": "INSTALL_VERSION", "installed": "INSTALL_VERSION",
"detached": "bool", "logo": "bool",
"description": "description" "state": "started|stopped",
} }
], ],
"addons_repositories": [ "addons_repositories": [
@@ -50,37 +54,10 @@ The addons from `addons` are only installed one.
} }
``` ```
- GET `/supervisor/addons`
Get all available addons
```json
{
"addons": [
{
"name": "xy bla",
"slug": "xy",
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool",
"description": "description"
}
],
"repositories": [
{
"slug": "12345678",
"name": "Repitory Name",
"source": "URL_OF_REPOSITORY",
"url": "null|WEBSITE",
"maintainer": "null|BLA BLU <fla@dld.ch>"
}
]
}
```
- POST `/supervisor/update` - POST `/supervisor/update`
Optional: Optional:
```json ```json
{ {
"version": "VERSION" "version": "VERSION"
@@ -88,9 +65,11 @@ Optional:
``` ```
- POST `/supervisor/options` - POST `/supervisor/options`
```json ```json
{ {
"beta_channel": "true|false", "beta_channel": "true|false",
"timezone": "TIMEZONE",
"addons_repositories": [ "addons_repositories": [
"REPO_URL" "REPO_URL"
] ]
@@ -103,64 +82,220 @@ Reload addons/version.
- GET `/supervisor/logs` - GET `/supervisor/logs`
Output the raw docker log Output is the raw docker log.
### Security
- GET `/security/info`
```json
{
"initialize": "bool",
"totp": "bool"
}
```
- POST `/security/options`
```json
{
"password": "xy"
}
```
- POST `/security/totp`
```json
{
"password": "xy"
}
```
Return QR-Code
- POST `/security/session`
```json
{
"password": "xy",
"totp": "null|123456"
}
```
### Backup/Snapshot
- GET `/snapshots`
```json
{
"snapshots": [
{
"slug": "SLUG",
"date": "ISO",
"name": "Custom name"
}
]
}
```
- POST `/snapshots/reload`
- POST `/snapshots/new/full`
```json
{
"name": "Optional"
}
```
- POST `/snapshots/new/partial`
```json
{
"name": "Optional",
"addons": ["ADDON_SLUG"],
"folders": ["FOLDER_NAME"]
}
```
- POST `/snapshots/reload`
- GET `/snapshots/{slug}/info`
```json
{
"slug": "SNAPSHOT ID",
"type": "full|partial",
"name": "custom snapshot name / description",
"date": "ISO",
"size": "SIZE_IN_MB",
"homeassistant": {
"version": "INSTALLED_HASS_VERSION",
"devices": []
},
"addons": [
{
"slug": "ADDON_SLUG",
"name": "NAME",
"version": "INSTALLED_VERSION"
}
],
"repositories": ["URL"],
"folders": ["NAME"]
}
```
- POST `/snapshots/{slug}/remove`
- POST `/snapshots/{slug}/restore/full`
- POST `/snapshots/{slug}/restore/partial`
```json
{
"homeassistant": "bool",
"addons": ["ADDON_SLUG"],
"folders": ["FOLDER_NAME"]
}
```
### Host ### Host
- POST `/host/reload`
- POST `/host/shutdown` - POST `/host/shutdown`
- POST `/host/reboot` - POST `/host/reboot`
- GET `/host/info` - GET `/host/info`
See HostControl info command.
```json ```json
{ {
"type": "", "type": "",
"version": "", "version": "",
"last_version": "", "last_version": "",
"features": ["shutdown", "reboot", "update", "network_info", "network_control"], "features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"],
"hostname": "", "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` - POST `/host/update`
Optional: Optional:
```json ```json
{ {
"version": "VERSION" "version": "VERSION"
} }
``` ```
- GET `/host/hardware`
```json
{
"serial": ["/dev/xy"],
"input": ["Input device name"],
"disk": ["/dev/sdax"],
"gpio": ["gpiochip0", "gpiochip100"],
"audio": {
"CARD_ID": {
"name": "xy",
"type": "microphone",
"devices": {
"DEV_ID": "type of device"
}
}
}
}
```
### Network ### Network
- GET `/network/info` - GET `/network/info`
- POST `/network/options`
```json ```json
{ {
"hostname": "", "hostname": ""
"mode": "dhcp|fixed",
"ssid": "",
"ip": "",
"netmask": "",
"gateway": ""
} }
``` ```
### HomeAssistant - POST `/network/options`
```json
{
"hostname": "",
}
```
### Home Assistant
- GET `/homeassistant/info` - GET `/homeassistant/info`
```json ```json
{ {
"version": "INSTALL_VERSION", "version": "INSTALL_VERSION",
"last_version": "LAST_VERSION" "last_version": "LAST_VERSION",
"devices": [""],
"image": "str",
"custom": "bool -> if custom image",
"boot": "bool",
"port": 8123,
"ssl": "bool",
"watchdog": "bool"
} }
``` ```
- POST `/homeassistant/update` - POST `/homeassistant/update`
Optional: Optional:
```json ```json
{ {
"version": "VERSION" "version": "VERSION"
@@ -169,61 +304,157 @@ Optional:
- GET `/homeassistant/logs` - GET `/homeassistant/logs`
Output the raw docker log Output is the raw Docker log.
### REST API addons - POST `/homeassistant/restart`
- POST `/homeassistant/check`
- POST `/homeassistant/start`
- POST `/homeassistant/stop`
- POST `/homeassistant/options`
- GET `/addons/{addon}/info`
```json ```json
{ {
"url": "null|url of addon", "devices": [],
"version": "VERSION", "image": "Optional|null",
"last_version": "LAST_VERSION", "last_version": "Optional for custom image|null",
"state": "started|stopped", "port": "port for access hass",
"boot": "auto|manual", "ssl": "bool",
"options": {}, "password": "",
"watchdog": "bool"
} }
``` ```
Image with `null` and last_version with `null` reset this options.
- POST/GET `/homeassistant/api`
Proxy to real home-assistant instance.
### 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",
"gpio": "bool",
"stdin": "bool",
"hassio_api": "bool",
"homeassistant_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",
"description": "description",
"auto_update": "bool",
"url": "null|url of addon",
"detached": "bool",
"repository": "12345678|null",
"version": "null|VERSION_INSTALLED",
"last_version": "LAST_VERSION",
"state": "none|started|stopped",
"boot": "auto|manual",
"build": "bool",
"options": "{}",
"network": "{}|null",
"host_network": "bool",
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
"devices": ["/dev/xy"],
"logo": "bool",
"hassio_api": "bool",
"homeassistant_api": "bool",
"stdin": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx",
"gpio": "bool",
"audio": "bool",
"audio_input": "null|0,0",
"audio_output": "null|0,0"
}
```
- GET `/addons/{addon}/logo`
- POST `/addons/{addon}/options` - POST `/addons/{addon}/options`
```json ```json
{ {
"boot": "auto|manual", "boot": "auto|manual",
"auto_update": "bool",
"network": {
"CONTAINER": "port|[ip, port]"
},
"options": {}, "options": {},
"audio_output": "null|0,0",
"audio_input": "null|0,0"
} }
``` ```
For reset custom network/audio settings, set it `null`.
- POST `/addons/{addon}/start` - POST `/addons/{addon}/start`
- POST `/addons/{addon}/stop` - POST `/addons/{addon}/stop`
- POST `/addons/{addon}/install` - POST `/addons/{addon}/install`
Optional:
```json
{
"version": "VERSION"
}
```
- POST `/addons/{addon}/uninstall` - POST `/addons/{addon}/uninstall`
- POST `/addons/{addon}/update` - POST `/addons/{addon}/update`
Optional:
```json
{
"version": "VERSION"
}
```
- GET `/addons/{addon}/logs` - 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
- POST `/addons/{addon}/stdin`
Write data to add-on stdin
## Host Control ## Host Control
Communicate over unix socket with a host daemon. Communicate over UNIX socket with a host daemon.
- commands - commands
``` ```
# info # info
-> {'type', 'version', 'last_version', 'features', 'hostname'} -> {'type', 'version', 'last_version', 'features', 'hostname'}
@@ -231,8 +462,10 @@ Communicate over unix socket with a host daemon.
# shutdown # shutdown
# host-update [v] # host-update [v]
# hostname xy
# network info # network info
# network hostname xy -> {}
# network wlan ssd xy # network wlan ssd xy
# network wlan password xy # network wlan password xy
# network int ip xy # network int ip xy
@@ -240,10 +473,12 @@ Communicate over unix socket with a host daemon.
# network int route xy # network int route xy
``` ```
features: Features:
- shutdown - shutdown
- reboot - reboot
- update - update
- hostname
- network_info - network_info
- network_control - network_control

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
ARG BUILD_FROM
FROM $BUILD_FROM
# add env
ENV LANG C.UTF-8
# setup base
RUN apk add --no-cache python3 python3-dev \
libressl libressl-dev \
libffi libffi-dev \
musl musl-dev \
gcc libstdc++ \
git socat \
&& pip3 install --no-cache-dir --upgrade pip \
&& pip3 install --no-cache-dir --upgrade cryptography jwcrypto \
&& apk del python3-dev libressl-dev libffi-dev musl-dev gcc
# install HassIO
COPY . /usr/src/hassio
RUN pip3 install --no-cache-dir /usr/src/hassio \
&& rm -rf /usr/src/hassio
CMD [ "python3", "-m", "hassio" ]

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 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
All rights reserved.
Redistribution and use in source and binary forms, with or without 1. Definitions.
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this "License" shall mean the terms and conditions for use, reproduction,
list of conditions and the following disclaimer. and distribution as defined by Sections 1 through 9 of this document.
* Redistributions in binary form must reproduce the above copyright notice, "Licensor" shall mean the copyright owner or entity authorized by
this list of conditions and the following disclaimer in the documentation the copyright owner that is granting the License.
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its "Legal Entity" shall mean the union of the acting entity and all
contributors may be used to endorse or promote products derived from other entities that control, are controlled by, or are under common
this software without specific prior written permission. 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" "You" (or "Your") shall mean an individual or Legal Entity
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE exercising permissions granted by this License.
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE "Source" form shall mean the preferred form for making modifications,
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL including but not limited to software source code, documentation
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR source, and configuration files.
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, "Object" form shall mean any form resulting from mechanical
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE transformation or translation of a Source form, including but
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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,31 +1,14 @@
# HassIO # Hass.io
### First private cloud solution for home automation. ### 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. 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) ![](misc/hassio.png?raw=true)
[HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build) - [Hass.io Addons](https://github.com/home-assistant/hassio-addons)
- [Hass.io Build](https://github.com/home-assistant/hassio-build)
**HassIO is under active development and is not ready yet for production use.** ## Installation
## Installing Hass.io Installation instructions can be found at [https://home-assistant.io/hassio](https://home-assistant.io/hassio).
- Generic Linux installation: https://github.com/home-assistant/hassio-build/tree/master/install
- Hardware Images: https://github.com/home-assistant/hassio-build/blob/master/meta-hassio/
## Feature in progress
- Backup/Restore
- DHCP-Server addon
# HomeAssistant
## SSL
All addons that create SSL certs follow the same file structure. If you use one, put follow lines in your `configuration.yaml`.
```yaml
http:
ssl_certificate: /ssl/fullchain.pem
ssl_key: /ssl/privkey.pem
```

View File

@@ -1,5 +1,6 @@
"""Main file for HassIO.""" """Main file for HassIO."""
import asyncio import asyncio
from concurrent.futures import ThreadPoolExecutor
import logging import logging
import sys import sys
@@ -12,22 +13,35 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=invalid-name # pylint: disable=invalid-name
if __name__ == "__main__": if __name__ == "__main__":
bootstrap.initialize_logging() bootstrap.initialize_logging()
loop = asyncio.get_event_loop()
if not bootstrap.check_environment(): if not bootstrap.check_environment():
exit(1) sys.exit(1)
loop = asyncio.get_event_loop() # init executor pool
hassio = core.HassIO(loop) executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
loop.set_default_executor(executor)
_LOGGER.info("Run Hassio setup") _LOGGER.info("Initialize Hassio setup")
config = bootstrap.initialize_system_data()
hassio = core.HassIO(loop, config)
bootstrap.migrate_system_env(config)
_LOGGER.info("Setup HassIO")
loop.run_until_complete(hassio.setup()) loop.run_until_complete(hassio.setup())
_LOGGER.info("Start Hassio task")
loop.call_soon_threadsafe(loop.create_task, hassio.start()) loop.call_soon_threadsafe(loop.create_task, hassio.start())
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, hassio) loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
loop.run_forever() try:
loop.close() _LOGGER.info("Run HassIO")
loop.run_forever()
finally:
_LOGGER.info("Stopping HassIO")
loop.run_until_complete(hassio.stop())
executor.shutdown(wait=False)
loop.close()
_LOGGER.info("Close Hassio") _LOGGER.info("Close Hassio")
sys.exit(hassio.exit_code) sys.exit(0)

View File

@@ -1,206 +1,133 @@
"""Init file for HassIO addons.""" """Init file for HassIO addons."""
import asyncio import asyncio
import logging import logging
import shutil
from .data import AddonsData from .addon import Addon
from .git import AddonsRepoHassIO, AddonsRepoCustom from .repository import Repository
from ..const import STATE_STOPPED, STATE_STARTED from .data import Data
from ..dock.addon import DockerAddon from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL, BOOT_AUTO
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
class AddonManager(AddonsData):
class AddonManager(object):
"""Manage addons inside HassIO.""" """Manage addons inside HassIO."""
def __init__(self, config, loop, dock): def __init__(self, config, loop, docker):
"""Initialize docker base wrapper.""" """Initialize docker base wrapper."""
super().__init__(config)
self.loop = loop self.loop = loop
self.dock = dock self.config = config
self.repositories = [] self.docker = docker
self.dockers = {} self.data = Data(config)
self.addons = {}
self.repositories = {}
async def prepare(self, arch): @property
def list_addons(self):
"""Return a list of all addons."""
return list(self.addons.values())
@property
def list_repositories(self):
"""Return list of addon repositories."""
return list(self.repositories.values())
def get(self, addon_slug):
"""Return a adddon from slug."""
return self.addons.get(addon_slug)
async def prepare(self):
"""Startup addon management.""" """Startup addon management."""
self.arch = arch self.data.reload()
# init hassio repository # init hassio built-in repositories
self.repositories.append(AddonsRepoHassIO(self.config, self.loop)) repositories = \
set(self.config.addons_repositories) | BUILTIN_REPOSITORIES
# init custom repositories # init custom repositories & load addons
for url in self.config.addons_repositories: await self.load_repositories(repositories)
self.repositories.append(
AddonsRepoCustom(self.config, self.loop, url))
# load addon repository
tasks = [addon.load() for addon in self.repositories]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# load installed addons
for addon in self.list_installed:
self.dockers[addon] = DockerAddon(
self.config, self.loop, self.dock, self, addon)
await self.dockers[addon].attach()
async def add_git_repository(self, url):
"""Add a new custom repository."""
if url in self.config.addons_repositories:
_LOGGER.warning("Repository already exists %s", url)
return False
repo = AddonsRepoCustom(self.config, self.loop, url)
if not await repo.load():
_LOGGER.error("Can't load from repository %s", url)
return False
self.config.addons_repositories = url
self.repositories.append(repo)
return True
def drop_git_repository(self, url):
"""Remove a custom repository."""
for repo in self.repositories:
if repo.url == url:
self.repositories.remove(repo)
self.config.drop_addon_repository(url)
repo.remove()
return True
return False
async def reload(self): async def reload(self):
"""Update addons from repo and reload list.""" """Update addons from repo and reload list."""
tasks = [addon.pull() for addon in self.repositories] tasks = [repository.update() for repository in
if not tasks: self.repositories.values()]
return
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# remove stalled addons
for addon in self.list_detached:
_LOGGER.warning("Dedicated addon '%s' found!", addon)
async def auto_boot(self, start_type):
"""Boot addons with mode auto."""
boot_list = self.list_startup(start_type)
tasks = [self.start(addon) for addon in boot_list]
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
if tasks: if tasks:
await asyncio.wait(tasks, loop=self.loop) await asyncio.wait(tasks, loop=self.loop)
async def install(self, addon, version=None): # read data from repositories
"""Install a addon.""" self.data.reload()
if not self.exists_addon(addon):
_LOGGER.error("Addon %s not exists for install", addon)
return False
if self.is_installed(addon): # update addons
_LOGGER.error("Addon %s is already installed", addon) await self.load_addons()
return False
if not self.path_data(addon).is_dir(): async def load_repositories(self, list_repositories):
_LOGGER.info("Create Home-Assistant addon data folder %s", """Add a new custom repository."""
self.path_data(addon)) new_rep = set(list_repositories)
self.path_data(addon).mkdir() old_rep = set(self.repositories)
addon_docker = DockerAddon( # add new repository
self.config, self.loop, self.dock, self, addon) async def _add_repository(url):
"""Helper function to async add repository."""
repository = Repository(self.config, self.loop, self.data, url)
if not await repository.load():
_LOGGER.error("Can't load from repository %s", url)
return
self.repositories[url] = repository
version = version or self.get_last_version(addon) # don't add built-in repository to config
if not await addon_docker.install(version): if url not in BUILTIN_REPOSITORIES:
return False self.config.add_addon_repository(url)
self.dockers[addon] = addon_docker tasks = [_add_repository(url) for url in new_rep - old_rep]
self.set_addon_install(addon, version) if tasks:
return True await asyncio.wait(tasks, loop=self.loop)
async def uninstall(self, addon): # del new repository
"""Remove a addon.""" for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
if not self.is_installed(addon): self.repositories.pop(url).remove()
_LOGGER.error("Addon %s is already uninstalled", addon) self.config.drop_addon_repository(url)
return False
if addon not in self.dockers: # update data
_LOGGER.error("No docker found for addon %s", addon) self.data.reload()
return False await self.load_addons()
if not await self.dockers[addon].remove(): async def load_addons(self):
return False """Update/add internal addon store."""
all_addons = set(self.data.system) | set(self.data.cache)
if self.path_data(addon).is_dir(): # calc diff
_LOGGER.info("Remove Home-Assistant addon data folder %s", add_addons = all_addons - set(self.addons)
self.path_data(addon)) del_addons = set(self.addons) - all_addons
shutil.rmtree(str(self.path_data(addon)))
self.dockers.pop(addon) _LOGGER.info("Load addons: %d all - %d new - %d remove",
self.set_addon_uninstall(addon) len(all_addons), len(add_addons), len(del_addons))
return True
async def state(self, addon): # new addons
"""Return running state of addon.""" tasks = []
if addon not in self.dockers: for addon_slug in add_addons:
_LOGGER.error("No docker found for addon %s", addon) addon = Addon(
return self.config, self.loop, self.docker, self.data, addon_slug)
if await self.dockers[addon].is_running(): tasks.append(addon.load())
return STATE_STARTED self.addons[addon_slug] = addon
return STATE_STOPPED
async def start(self, addon): if tasks:
"""Set options and start addon.""" await asyncio.wait(tasks, loop=self.loop)
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
if not self.write_addon_options(addon): # remove
_LOGGER.error("Can't write options for addon %s", addon) for addon_slug in del_addons:
return False self.addons.pop(addon_slug)
return await self.dockers[addon].run() async def auto_boot(self, stage):
"""Boot addons with mode auto."""
tasks = []
for addon in self.addons.values():
if addon.is_installed and addon.boot == BOOT_AUTO and \
addon.startup == stage:
tasks.append(addon.start())
async def stop(self, addon): _LOGGER.info("Startup %s run %d addons", stage, len(tasks))
"""Stop addon.""" if tasks:
if addon not in self.dockers: await asyncio.wait(tasks, loop=self.loop)
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].stop()
async def update(self, addon, version=None):
"""Update addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
version = version or self.get_last_version(addon)
is_running = self.dockers[addon].is_running()
# update
if await self.dockers[addon].update(version):
self.set_addon_update(addon, version)
if is_running:
await self.start(addon)
return True
return False
async def logs(self, addon):
"""Return addons log output."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].logs()

689
hassio/addons/addon.py Normal file
View File

@@ -0,0 +1,689 @@
"""Init file for HassIO addons."""
from copy import deepcopy
import logging
import json
from pathlib import Path, PurePath
import re
import shutil
import tarfile
from tempfile import TemporaryDirectory
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import (
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, ATTR_UUID,
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
from .util import check_installed
from ..dock.addon import DockerAddon
from ..tools import write_json_file, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_WEBUI = re.compile(
r"^(?:(?P<s_prefix>https?)|\[PROTO:(?P<t_proto>\w+)\])"
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$")
class Addon(object):
"""Hold data for addon inside HassIO."""
def __init__(self, config, loop, docker, data, slug):
"""Initialize data holder."""
self.loop = loop
self.config = config
self.data = data
self._id = slug
self.docker = DockerAddon(config, loop, docker, self)
async def load(self):
"""Async initialize of object."""
if self.is_installed:
await self.docker.attach()
@property
def slug(self):
"""Return slug/id of addon."""
return self._id
@property
def _mesh(self):
"""Return addon data from system or cache."""
return self.data.system.get(self._id, self.data.cache.get(self._id))
@property
def is_installed(self):
"""Return True if a addon is installed."""
return self._id in self.data.system
@property
def is_detached(self):
"""Return True if addon is detached."""
return self._id not in self.data.cache
@property
def version_installed(self):
"""Return installed version."""
return self.data.user.get(self._id, {}).get(ATTR_VERSION)
def _set_install(self, version):
"""Set addon as installed."""
self.data.system[self._id] = deepcopy(self.data.cache[self._id])
self.data.user[self._id] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self.data.save()
def _set_uninstall(self):
"""Set addon as uninstalled."""
self.data.system.pop(self._id, None)
self.data.user.pop(self._id, None)
self.data.save()
def _set_update(self, version):
"""Update version of addon."""
self.data.system[self._id] = deepcopy(self.data.cache[self._id])
self.data.user[self._id][ATTR_VERSION] = version
self.data.save()
def _restore_data(self, user, system):
"""Restore data to addon."""
self.data.user[self._id] = deepcopy(user)
self.data.system[self._id] = deepcopy(system)
self.data.save()
@property
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 self.data.cache[self._id][ATTR_OPTIONS]
@options.setter
def options(self, value):
"""Store user addon options."""
self.data.user[self._id][ATTR_OPTIONS] = deepcopy(value)
self.data.save()
@property
def boot(self):
"""Return boot config with prio local settings."""
if ATTR_BOOT in self.data.user.get(self._id, {}):
return self.data.user[self._id][ATTR_BOOT]
return self._mesh[ATTR_BOOT]
@boot.setter
def boot(self, value):
"""Store user boot options."""
self.data.user[self._id][ATTR_BOOT] = value
self.data.save()
@property
def auto_update(self):
"""Return if auto update is enable."""
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):
"""Set auto update."""
self.data.user[self._id][ATTR_AUTO_UPDATE] = value
self.data.save()
@property
def name(self):
"""Return name of addon."""
return self._mesh[ATTR_NAME]
@property
def timeout(self):
"""Return timeout of addon for docker stop."""
return self._mesh[ATTR_TIMEOUT]
@property
def api_token(self):
"""Return a API token for this add-on."""
if self.is_installed:
return self.data.user[self._id][ATTR_UUID]
@property
def description(self):
"""Return description of addon."""
return self._mesh[ATTR_DESCRIPTON]
@property
def repository(self):
"""Return repository of addon."""
return self._mesh[ATTR_REPOSITORY]
@property
def last_version(self):
"""Return version of addon."""
if self._id in self.data.cache:
return self.data.cache[self._id][ATTR_VERSION]
return self.version_installed
@property
def startup(self):
"""Return startup type of addon."""
return self._mesh.get(ATTR_STARTUP)
@property
def ports(self):
"""Return ports of addon."""
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]:
return self._mesh[ATTR_PORTS]
return self.data.user[self._id][ATTR_NETWORK]
@ports.setter
def ports(self, value):
"""Set custom ports of addon."""
if value is None:
self.data.user[self._id].pop(ATTR_NETWORK, None)
else:
new_ports = {}
for container_port, host_port in value.items():
if container_port in self._mesh.get(ATTR_PORTS, {}):
new_ports[container_port] = host_port
self.data.user[self._id][ATTR_NETWORK] = new_ports
self.data.save()
@property
def webui(self):
"""Return URL to webui or None."""
if ATTR_WEBUI not in self._mesh:
return None
webui = RE_WEBUI.match(self._mesh[ATTR_WEBUI])
# extract arguments
t_port = webui.group('t_port')
t_proto = webui.group('t_proto')
s_prefix = webui.group('s_prefix') or ""
s_suffix = webui.group('s_suffix') or ""
# search host port for this docker port
if self.ports is None:
port = t_port
else:
port = self.ports.get("{}/tcp".format(t_port), t_port)
# for interface config or port lists
if isinstance(port, (tuple, list)):
port = port[-1]
# lookup the correct protocol from config
if t_proto:
proto = 'https' if self.options[t_proto] else 'http'
else:
proto = s_prefix
return "{}://[HOST]:{}{}".format(proto, port, s_suffix)
@property
def host_network(self):
"""Return True if addon run on host network."""
return self._mesh[ATTR_HOST_NETWORK]
@property
def devices(self):
"""Return devices of addon."""
return self._mesh.get(ATTR_DEVICES)
@property
def tmpfs(self):
"""Return tmpfs of addon."""
return self._mesh.get(ATTR_TMPFS)
@property
def environment(self):
"""Return environment of addon."""
return self._mesh.get(ATTR_ENVIRONMENT)
@property
def privileged(self):
"""Return list of privilege."""
return self._mesh.get(ATTR_PRIVILEGED)
@property
def legacy(self):
"""Return if the add-on don't support hass labels."""
return self._mesh.get(ATTR_LEGACY)
@property
def access_hassio_api(self):
"""Return True if the add-on access to hassio api."""
return self._mesh[ATTR_HASSIO_API]
@property
def access_homeassistant_api(self):
"""Return True if the add-on access to Home-Assistant api proxy."""
return self._mesh[ATTR_HOMEASSISTANT_API]
@property
def with_stdin(self):
"""Return True if the add-on access use stdin input."""
return self._mesh[ATTR_STDIN]
@property
def with_gpio(self):
"""Return True if the add-on access to gpio interface."""
return self._mesh[ATTR_GPIO]
@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."""
return self._mesh[ATTR_ARCH]
@property
def image(self):
"""Return image name of addon."""
addon_data = self._mesh
# Repository with dockerhub images
if ATTR_IMAGE in addon_data:
return addon_data[ATTR_IMAGE].format(arch=self.config.arch)
# local build
return "{}/{}-addon-{}".format(
addon_data[ATTR_REPOSITORY], self.config.arch,
addon_data[ATTR_SLUG])
@property
def need_build(self):
"""Return True if this addon need a local build."""
return ATTR_IMAGE not in self._mesh
@property
def map_volumes(self):
"""Return a dict of {volume: policy} from addon."""
volumes = {}
for volume in self._mesh[ATTR_MAP]:
result = RE_VOLUME.match(volume)
volumes[result.group(1)] = result.group(2) or 'ro'
return volumes
@property
def path_data(self):
"""Return addon data path inside supervisor."""
return Path(self.config.path_addons_data, self._id)
@property
def path_extern_data(self):
"""Return addon data path external for docker."""
return PurePath(self.config.path_extern_addons_data, self._id)
@property
def path_options(self):
"""Return path to addons options."""
return Path(self.path_data, "options.json")
@property
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
options = self.options
try:
schema(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))
return False
@property
def schema(self):
"""Create a schema for addon options."""
raw_schema = self._mesh[ATTR_SCHEMA]
if isinstance(raw_schema, bool):
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):
"""Install a addon."""
if self.config.arch not in self.supported_arch:
_LOGGER.error(
"Addon %s not supported on %s", self._id, self.config.arch)
return False
if self.is_installed:
_LOGGER.error("Addon %s is already installed", self._id)
return False
if not self.path_data.is_dir():
_LOGGER.info(
"Create Home-Assistant addon data folder %s", self.path_data)
self.path_data.mkdir()
if not await self.docker.install(self.last_version):
return False
self._set_install(self.last_version)
return True
@check_installed
async def uninstall(self):
"""Remove a addon."""
if not await self.docker.remove():
return False
if self.path_data.is_dir():
_LOGGER.info(
"Remove Home-Assistant addon data folder %s", self.path_data)
shutil.rmtree(str(self.path_data))
self._set_uninstall()
return True
async def state(self):
"""Return running state of addon."""
if not self.is_installed:
return STATE_NONE
if await self.docker.is_running():
return STATE_STARTED
return STATE_STOPPED
@check_installed
def start(self):
"""Set options and start addon.
Return a coroutine.
"""
return self.docker.run()
@check_installed
def stop(self):
"""Stop addon.
Return a coroutine.
"""
return self.docker.stop()
@check_installed
async def update(self):
"""Update addon."""
last_state = await self.state()
if self.last_version == self.version_installed:
_LOGGER.warning(
"No update available for Addon %s", self._id)
return False
if not await self.docker.update(self.last_version):
return False
self._set_update(self.last_version)
# restore state
if last_state == STATE_STARTED:
await self.docker.run()
return True
@check_installed
def restart(self):
"""Restart addon.
Return a coroutine.
"""
return self.docker.restart()
@check_installed
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 write_stdin(self, data):
"""Write data to add-on stdin.
Return a coroutine.
"""
if not self.with_stdin:
_LOGGER.error("Add-on don't support write to stdin!")
return False
return await self.docker.write_stdin(data)
@check_installed
async def snapshot(self, tar_file):
"""Snapshot a state of a addon."""
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
# store local image
if self.need_build and not await \
self.docker.export_image(Path(temp, "image.tar")):
return False
data = {
ATTR_USER: self.data.user.get(self._id, {}),
ATTR_SYSTEM: self.data.system.get(self._id, {}),
ATTR_VERSION: self.version_installed,
ATTR_STATE: await self.state(),
}
# store local configs/state
if not write_json_file(Path(temp, "addon.json"), data):
_LOGGER.error("Can't write addon.json for %s", self._id)
return False
# write into tarfile
def _create_tar():
"""Write tar inside loop."""
with tarfile.open(tar_file, "w:gz",
compresslevel=1) as snapshot:
snapshot.add(temp, arcname=".")
snapshot.add(self.path_data, arcname="data")
try:
_LOGGER.info("Build snapshot for addon %s", self._id)
await self.loop.run_in_executor(None, _create_tar)
except tarfile.TarError as err:
_LOGGER.error("Can't write tarfile %s -> %s", tar_file, err)
return False
_LOGGER.info("Finish snapshot for addon %s", self._id)
return True
async def restore(self, tar_file):
"""Restore a state of a addon."""
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
# extract snapshot
def _extract_tar():
"""Extract tar snapshot."""
with tarfile.open(tar_file, "r:gz") as snapshot:
snapshot.extractall(path=Path(temp))
try:
await self.loop.run_in_executor(None, _extract_tar)
except tarfile.TarError as err:
_LOGGER.error("Can't read tarfile %s -> %s", tar_file, err)
return False
# read snapshot data
try:
data = read_json_file(Path(temp, "addon.json"))
except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't read addon.json -> %s", err)
# validate
try:
data = SCHEMA_ADDON_SNAPSHOT(data)
except vol.Invalid as err:
_LOGGER.error("Can't validate %s, snapshot data -> %s",
self._id, humanize_error(data, err))
return False
# restore data / reload addon
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM])
# check version / restore image
version = data[ATTR_VERSION]
if version != self.docker.version:
image_file = Path(temp, "image.tar")
if image_file.is_file():
await self.docker.import_image(image_file, version)
else:
if await self.docker.install(version):
await self.docker.cleanup()
else:
await self.docker.stop()
# restore data
def _restore_data():
"""Restore data."""
if self.path_data.is_dir():
shutil.rmtree(str(self.path_data), ignore_errors=True)
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
try:
_LOGGER.info("Restore data for addon %s", self._id)
await self.loop.run_in_executor(None, _restore_data)
except shutil.Error as err:
_LOGGER.error("Can't restore origin data -> %s", err)
return False
# run addon
if data[ATTR_STATE] == STATE_STARTED:
return await self.start()
_LOGGER.info("Finish restore for addon %s", self._id)
return True

65
hassio/addons/build.py Normal file
View File

@@ -0,0 +1,65 @@
"""HassIO addons build environment."""
from pathlib import Path
from .validate import SCHEMA_BUILD_CONFIG
from ..const import ATTR_SQUASH, ATTR_BUILD_FROM, ATTR_ARGS, META_ADDON
from ..tools import JsonConfig
class AddonBuild(JsonConfig):
"""Handle build options for addons."""
def __init__(self, config, addon):
"""Initialize addon builder."""
self.config = config
self.addon = addon
super().__init__(
Path(addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
def save(self):
"""Ignore save function."""
pass
@property
def base_image(self):
"""Base images for this addon."""
return self._data[ATTR_BUILD_FROM][self.config.arch]
@property
def squash(self):
"""Return True or False if squash is active."""
return self._data[ATTR_SQUASH]
@property
def additional_args(self):
"""Return additional docker build arguments."""
return self._data[ATTR_ARGS]
def get_docker_args(self, version):
"""Create a dict with docker build arguments."""
args = {
'path': str(self.addon.path_location),
'tag': "{}:{}".format(self.addon.image, version),
'pull': True,
'forcerm': True,
'squash': self.squash,
'labels': {
'io.hass.version': version,
'io.hass.arch': self.config.arch,
'io.hass.type': META_ADDON,
'io.hass.name': self.addon.name,
'io.hass.description': self.addon.description,
},
'buildargs': {
'BUILD_FROM': self.base_image,
'BUILD_VERSION': version,
'BUILD_ARCH': self.config.arch,
**self.additional_args,
}
}
if self.addon.url:
args['labels']['io.hass.url'] = self.addon.url
return args

View File

@@ -1,12 +1,10 @@
{ {
"local": { "local": {
"slug": "local",
"name": "Local Add-Ons", "name": "Local Add-Ons",
"url": "https://home-assistant.io/hassio", "url": "https://home-assistant.io/hassio",
"maintainer": "By our self" "maintainer": "By our self"
}, },
"core": { "core": {
"slug": "core",
"name": "Built-in Add-Ons", "name": "Built-in Add-Ons",
"url": "https://home-assistant.io/addons", "url": "https://home-assistant.io/addons",
"maintainer": "Home Assistant authors" "maintainer": "Home Assistant authors"

View File

@@ -2,56 +2,56 @@
import copy import copy
import logging import logging
import json import json
from pathlib import Path, PurePath from pathlib import Path
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .util import extract_hash_from_path from .util import extract_hash_from_path
from .validate import ( from .validate import (
validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG) SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG)
from ..const import ( from ..const import (
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO, REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS, from ..tools import JsonConfig, read_json_file
MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL)
from ..config import Config
from ..tools import read_json_file, write_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SYSTEM = 'system'
USER = 'user'
REPOSITORY_CORE = 'core' class Data(JsonConfig):
REPOSITORY_LOCAL = 'local'
class AddonsData(Config):
"""Hold data for addons inside HassIO.""" """Hold data for addons inside HassIO."""
def __init__(self, config): def __init__(self, config):
"""Initialize data holder.""" """Initialize data holder."""
super().__init__(FILE_HASSIO_ADDONS) super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDON_FILE)
self.config = config self.config = config
self._system_data = self._data.get(SYSTEM, {}) self._repositories = {}
self._user_data = self._data.get(USER, {}) self._cache = {}
self._addons_cache = {}
self._repositories_data = {}
self.arch = None
def save(self): @property
"""Store data to config file.""" def user(self):
self._data = { """Return local addon user data."""
USER: self._user_data, return self._data[ATTR_USER]
SYSTEM: self._system_data,
}
super().save()
def read_data_from_repositories(self): @property
def system(self):
"""Return local addon data."""
return self._data[ATTR_SYSTEM]
@property
def cache(self):
"""Return addon data from cache/repositories."""
return self._cache
@property
def repositories(self):
"""Return addon data from repositories."""
return self._repositories
def reload(self):
"""Read data from addons repository.""" """Read data from addons repository."""
self._addons_cache = {} self._cache = {}
self._repositories_data = {} self._repositories = {}
# read core repository # read core repository
self._read_addons_folder( self._read_addons_folder(
@@ -69,19 +69,21 @@ class AddonsData(Config):
if repository_element.is_dir(): if repository_element.is_dir():
self._read_git_repository(repository_element) self._read_git_repository(repository_element)
# update local data
self._merge_config()
def _read_git_repository(self, path): def _read_git_repository(self, path):
"""Process a custom repository folder.""" """Process a custom repository folder."""
slug = extract_hash_from_path(path) slug = extract_hash_from_path(path)
repository_info = {ATTR_SLUG: slug}
# exists repository json # exists repository json
repository_file = Path(path, "repository.json") repository_file = Path(path, "repository.json")
try: try:
repository_info.update(SCHEMA_REPOSITORY_CONFIG( repository_info = SCHEMA_REPOSITORY_CONFIG(
read_json_file(repository_file) read_json_file(repository_file)
)) )
except OSError: except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read repository information from %s", _LOGGER.warning("Can't read repository information from %s",
repository_file) repository_file)
return return
@@ -91,7 +93,7 @@ class AddonsData(Config):
return return
# process data # process data
self._repositories_data[slug] = repository_info self._repositories[slug] = repository_info
self._read_addons_folder(path, slug) self._read_addons_folder(path, slug)
def _read_addons_folder(self, path, repository): def _read_addons_folder(self, path, repository):
@@ -109,9 +111,10 @@ class AddonsData(Config):
# store # store
addon_config[ATTR_REPOSITORY] = repository addon_config[ATTR_REPOSITORY] = repository
self._addons_cache[addon_slug] = addon_config 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) _LOGGER.warning("Can't read %s", addon)
except vol.Invalid as ex: except vol.Invalid as ex:
@@ -127,225 +130,32 @@ class AddonsData(Config):
_LOGGER.warning("Can't read built-in.json -> %s", err) _LOGGER.warning("Can't read built-in.json -> %s", err)
return return
# if core addons are available # core repository
for data in self._addons_cache.values(): self._repositories[REPOSITORY_CORE] = \
if data[ATTR_REPOSITORY] == REPOSITORY_CORE: builtin_data[REPOSITORY_CORE]
self._repositories_data[REPOSITORY_CORE] = \
builtin_data[REPOSITORY_CORE]
break
# if local addons are available # local repository
for data in self._addons_cache.values(): self._repositories[REPOSITORY_LOCAL] = \
if data[ATTR_REPOSITORY] == REPOSITORY_LOCAL: builtin_data[REPOSITORY_LOCAL]
self._repositories_data[REPOSITORY_LOCAL] = \
builtin_data[REPOSITORY_LOCAL]
break
def merge_update_config(self): def _merge_config(self):
"""Update local config if they have update. """Update local config if they have update.
It need to be the same version as the local version is. It need to be the same version as the local version is for merge.
""" """
have_change = False have_change = False
for addon, data in self._system_data.items(): for addon in set(self.system):
# detached # detached
if addon not in self._addons_cache: if addon not in self._cache:
continue continue
cache = self._addons_cache[addon] cache = self._cache[addon]
data = self.system[addon]
if data[ATTR_VERSION] == cache[ATTR_VERSION]: if data[ATTR_VERSION] == cache[ATTR_VERSION]:
if data != cache: if data != cache:
self._system_data[addon] = copy.deepcopy(cache) self.system[addon] = copy.deepcopy(cache)
have_change = True have_change = True
if have_change: if have_change:
self.save() self.save()
@property
def list_installed(self):
"""Return a list of installed addons."""
return set(self._system_data.keys())
@property
def data_all(self):
"""Return a dict of all addons."""
return {
**self._system_data,
**self._addons_cache
}
@property
def data_installed(self):
"""Return a dict of installed addons."""
return self._system_data.copy()
def list_startup(self, start_type):
"""Get list of installed addon with need start by type."""
addon_list = set()
for addon in self._system_data.keys():
if self.get_boot(addon) != BOOT_AUTO:
continue
try:
if self._system_data[addon][ATTR_STARTUP] == start_type:
addon_list.add(addon)
except KeyError:
_LOGGER.warning("Orphaned addon detect %s", addon)
continue
return addon_list
@property
def list_detached(self):
"""Return local addons they not support from repo."""
addon_list = set()
for addon in self._system_data.keys():
if addon not in self._addons_cache:
addon_list.add(addon)
return addon_list
@property
def list_repositories(self):
"""Return list of addon repositories."""
return list(self._repositories_data.values())
def exists_addon(self, addon):
"""Return True if a addon exists."""
return addon in self._addons_cache or addon in self._system_data
def is_installed(self, addon):
"""Return True if a addon is installed."""
return addon in self._system_data
def version_installed(self, addon):
"""Return installed version."""
return self._user_data.get(addon, {}).get(ATTR_VERSION)
def set_addon_install(self, addon, version):
"""Set addon as installed."""
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
self._user_data[addon] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self.save()
def set_addon_uninstall(self, addon):
"""Set addon as uninstalled."""
self._system_data.pop(addon, None)
self._user_data.pop(addon, None)
self.save()
def set_addon_update(self, addon, version):
"""Update version of addon."""
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
self._user_data[addon][ATTR_VERSION] = version
self.save()
def set_options(self, addon, options):
"""Store user addon options."""
self._user_data[addon][ATTR_OPTIONS] = copy.deepcopy(options)
self.save()
def set_boot(self, addon, boot):
"""Store user boot options."""
self._user_data[addon][ATTR_BOOT] = boot
self.save()
def get_options(self, addon):
"""Return options with local changes."""
return {
**self._system_data[addon][ATTR_OPTIONS],
**self._user_data[addon][ATTR_OPTIONS],
}
def get_boot(self, addon):
"""Return boot config with prio local settings."""
if ATTR_BOOT in self._user_data[addon]:
return self._user_data[addon][ATTR_BOOT]
return self._system_data[addon][ATTR_BOOT]
def get_name(self, addon):
"""Return name of addon."""
return self._system_data[addon][ATTR_NAME]
def get_description(self, addon):
"""Return description of addon."""
return self._system_data[addon][ATTR_DESCRIPTON]
def get_last_version(self, addon):
"""Return version of addon."""
if addon not in self._addons_cache:
return self.version_installed(addon)
return self._addons_cache[addon][ATTR_VERSION]
def get_ports(self, addon):
"""Return ports of addon."""
return self._system_data[addon].get(ATTR_PORTS)
def get_url(self, addon):
"""Return url of addon."""
return self._system_data[addon].get(ATTR_URL)
def get_image(self, addon):
"""Return image name of addon."""
addon_data = self._system_data.get(
addon, self._addons_cache.get(addon))
if ATTR_IMAGE not in addon_data:
return "{}/{}-addon-{}".format(
DOCKER_REPO, self.arch, addon_data[ATTR_SLUG])
return addon_data[ATTR_IMAGE].format(arch=self.arch)
def map_config(self, addon):
"""Return True if config map is needed."""
return MAP_CONFIG in self._system_data[addon][ATTR_MAP]
def map_ssl(self, addon):
"""Return True if ssl map is needed."""
return MAP_SSL in self._system_data[addon][ATTR_MAP]
def map_addons(self, addon):
"""Return True if addons map is needed."""
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
def map_backup(self, addon):
"""Return True if backup map is needed."""
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
def path_data(self, addon):
"""Return addon data path inside supervisor."""
return Path(self.config.path_addons_data, addon)
def path_extern_data(self, addon):
"""Return addon data path external for docker."""
return str(PurePath(self.config.path_extern_addons_data, addon))
def path_addon_options(self, addon):
"""Return path to addons options."""
return Path(self.path_data(addon), "options.json")
def write_addon_options(self, addon):
"""Return True if addon options is written to data."""
schema = self.get_schema(addon)
options = self.get_options(addon)
try:
schema(options)
return write_json_file(self.path_addon_options(addon), options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options -> %s", addon,
humanize_error(options, ex))
return False
def get_schema(self, addon):
"""Create a schema for addon options."""
raw_schema = self._system_data[addon][ATTR_SCHEMA]
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
return schema

View File

@@ -1,6 +1,7 @@
"""Init file for HassIO addons git.""" """Init file for HassIO addons git."""
import asyncio import asyncio
import logging import logging
import functools as ft
from pathlib import Path from pathlib import Path
import shutil import shutil
@@ -12,7 +13,7 @@ from ..const import URL_HASSIO_ADDONS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class AddonsRepo(object): class GitRepo(object):
"""Manage addons git repo.""" """Manage addons git repo."""
def __init__(self, config, loop, path, url): def __init__(self, config, loop, path, url):
@@ -48,7 +49,9 @@ class AddonsRepo(object):
try: try:
_LOGGER.info("Clone addon %s repository", self.url) _LOGGER.info("Clone addon %s repository", self.url)
self.repo = await self.loop.run_in_executor( self.repo = await self.loop.run_in_executor(
None, git.Repo.clone_from, self.url, str(self.path)) None, ft.partial(
git.Repo.clone_from, self.url, str(self.path),
recursive=True))
except (git.InvalidGitRepositoryError, git.NoSuchPathError, except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.GitCommandError) as err: git.GitCommandError) as err:
@@ -70,14 +73,14 @@ class AddonsRepo(object):
None, self.repo.remotes.origin.pull) None, self.repo.remotes.origin.pull)
except (git.InvalidGitRepositoryError, git.NoSuchPathError, except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.exc.GitCommandError) as err: git.GitCommandError) as err:
_LOGGER.error("Can't pull %s repo: %s.", self.url, err) _LOGGER.error("Can't pull %s repo: %s.", self.url, err)
return False return False
return True return True
class AddonsRepoHassIO(AddonsRepo): class GitRepoHassIO(GitRepo):
"""HassIO addons repository.""" """HassIO addons repository."""
def __init__(self, config, loop): def __init__(self, config, loop):
@@ -86,7 +89,7 @@ class AddonsRepoHassIO(AddonsRepo):
config, loop, config.path_addons_core, URL_HASSIO_ADDONS) config, loop, config.path_addons_core, URL_HASSIO_ADDONS)
class AddonsRepoCustom(AddonsRepo): class GitRepoCustom(GitRepo):
"""Custom addons repository.""" """Custom addons repository."""
def __init__(self, config, loop, url): def __init__(self, config, loop, url):

View File

@@ -0,0 +1,71 @@
"""Represent a HassIO repository."""
from .git import GitRepoHassIO, GitRepoCustom
from .util import get_hash_from_repository
from ..const import (
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
UNKNOWN = 'unknown'
class Repository(object):
"""Repository in HassIO."""
def __init__(self, config, loop, data, repository):
"""Initialize repository object."""
self.data = data
self.source = None
self.git = None
if repository == REPOSITORY_LOCAL:
self._id = repository
elif repository == REPOSITORY_CORE:
self._id = repository
self.git = GitRepoHassIO(config, loop)
else:
self._id = get_hash_from_repository(repository)
self.git = GitRepoCustom(config, loop, repository)
self.source = repository
@property
def _mesh(self):
"""Return data struct repository."""
return self.data.repositories.get(self._id, {})
@property
def slug(self):
"""Return slug of repository."""
return self._id
@property
def name(self):
"""Return name of repository."""
return self._mesh.get(ATTR_NAME, UNKNOWN)
@property
def url(self):
"""Return url of repository."""
return self._mesh.get(ATTR_URL, self.source)
@property
def maintainer(self):
"""Return url of repository."""
return self._mesh.get(ATTR_MAINTAINER, UNKNOWN)
async def load(self):
"""Load addon repository."""
if self.git:
return await self.git.load()
return True
async def update(self):
"""Update addon repository."""
if self.git:
return await self.git.pull()
return True
def remove(self):
"""Remove addon repository."""
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
raise RuntimeError("Can't remove built-in repositories!")
self.git.remove()

View File

@@ -1,10 +1,12 @@
"""Util addons functions.""" """Util addons functions."""
import hashlib import hashlib
import logging
import re import re
RE_SLUGIFY = re.compile(r'[^a-z0-9_]+')
RE_SHA1 = re.compile(r"[a-f0-9]{8}") RE_SHA1 = re.compile(r"[a-f0-9]{8}")
_LOGGER = logging.getLogger(__name__)
def get_hash_from_repository(name): def get_hash_from_repository(name):
"""Generate a hash from repository.""" """Generate a hash from repository."""
@@ -21,6 +23,13 @@ def extract_hash_from_path(path):
return repo_dir return repo_dir
def create_hash_index_list(name_list): def check_installed(method):
"""Create a dict with hash from repositories list.""" """Wrap function with check if addon is installed."""
return {get_hash_from_repository(repo): repo for repo in name_list} async def wrap_check(addon, *args, **kwargs):
"""Return False if not installed or the function."""
if not addon.is_installed:
_LOGGER.error("Addon %s is not installed", addon.slug)
return False
return await method(addon, *args, **kwargs)
return wrap_check

View File

@@ -1,11 +1,28 @@
"""Validate addons options schema.""" """Validate addons options schema."""
import logging
import re
import uuid
import voluptuous as vol import voluptuous as vol
from ..const import ( from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE,
STARTUP_BEFORE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, MAP_SSL, STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE,
MAP_CONFIG, MAP_ADDONS, MAP_BACKUP, ATTR_URL, ATTR_MAINTAINER) 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_UUID,
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
V_STR = 'str' V_STR = 'str'
V_INT = 'int' V_INT = 'int'
@@ -13,8 +30,51 @@ V_FLOAT = 'float'
V_BOOL = 'bool' V_BOOL = 'bool'
V_EMAIL = 'email' V_EMAIL = 'email'
V_URL = 'url' V_URL = 'url'
V_PORT = 'port'
V_MATCH = 'match'
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
]
STARTUP_ALL = [
STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES,
STARTUP_APPLICATION
]
PRIVILEGED_ALL = [
"NET_ADMIN",
"SYS_ADMIN",
"SYS_RAWIO"
]
BASE_IMAGE = {
ARCH_ARMHF: "homeassistant/armhf-base:latest",
ARCH_AARCH64: "homeassistant/aarch64-base:latest",
ARCH_I386: "homeassistant/i386-base:latest",
ARCH_AMD64: "homeassistant/amd64-base:latest",
}
def _simple_startup(value):
"""Simple startup schema."""
if value == "before":
return STARTUP_SERVICES
if value == "after":
return STARTUP_APPLICATION
return value
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL])
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_ADDON_CONFIG = vol.Schema({ SCHEMA_ADDON_CONFIG = vol.Schema({
@@ -22,23 +82,43 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_SLUG): vol.Coerce(str), vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
vol.Required(ATTR_STARTUP): vol.Required(ATTR_STARTUP):
vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]), vol.All(_simple_startup, vol.In(STARTUP_ALL)),
vol.Required(ATTR_BOOT): vol.Required(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): dict, vol.Optional(ATTR_PORTS): DOCKER_PORTS,
vol.Optional(ATTR_MAP, default=[]): [ vol.Optional(ATTR_WEBUI):
vol.In([MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP]) vol.Match(r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"),
], vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_URL): vol.Url(), 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(RE_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): { vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT}) vol.Any(
]) SCHEMA_ELEMENT,
}, {vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"), ),
}, extra=vol.ALLOW_EXTRA) ], vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [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)),
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
@@ -46,7 +126,58 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_MAINTAINER): vol.Coerce(str), vol.Optional(ATTR_MAINTAINER): vol.Coerce(str),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_BUILD_CONFIG = vol.Schema({
vol.Optional(ATTR_BUILD_FROM, default=BASE_IMAGE): vol.Schema({
vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"),
}),
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
vol.Optional(ATTR_ARGS, default={}): vol.Schema({
vol.Coerce(str): vol.Coerce(str)
}),
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
vol.Match(r"^[0-9a-f]{32}$"),
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,
}, extra=vol.REMOVE_EXTRA)
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
vol.Required(ATTR_LOCATON): vol.Coerce(str),
vol.Required(ATTR_REPOSITORY): vol.Coerce(str),
})
SCHEMA_ADDON_FILE = vol.Schema({
vol.Optional(ATTR_USER, default={}): {
vol.Coerce(str): SCHEMA_ADDON_USER,
},
vol.Optional(ATTR_SYSTEM, default={}): {
vol.Coerce(str): SCHEMA_ADDON_SYSTEM,
}
})
SCHEMA_ADDON_SNAPSHOT = vol.Schema({
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]),
vol.Required(ATTR_VERSION): vol.Coerce(str),
})
def validate_options(raw_schema): def validate_options(raw_schema):
@@ -57,70 +188,111 @@ def validate_options(raw_schema):
# read options # read options
for key, value in struct.items(): for key, value in struct.items():
# Ignore unknown options / remove from list
if key not in raw_schema: if key not in raw_schema:
raise vol.Invalid("Unknown options {}.".format(key)) _LOGGER.warning("Unknown options %s", key)
continue
typ = raw_schema[key] typ = raw_schema[key]
try: try:
if isinstance(typ, list): if isinstance(typ, list):
# nested value # nested value list
options[key] = _nested_validate(typ[0], value) 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: else:
# normal value # normal value
options[key] = _single_validate(typ, value) options[key] = _single_validate(typ, value, key)
except (IndexError, KeyError): except (IndexError, KeyError):
raise vol.Invalid( raise vol.Invalid(
"Type error for {}.".format(key)) from None "Type error for {}.".format(key)) from None
_check_missing_options(raw_schema, options, 'root')
return options return options
return validate return validate
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
def _single_validate(typ, value): def _single_validate(typ, value, key):
"""Validate a single element.""" """Validate a single element."""
try: # if required argument
# if required argument if value is None:
if value is None: raise vol.Invalid("Missing required option '{}'.".format(key))
raise vol.Invalid("A required argument is not set!")
if typ == V_STR: # parse extend data from type
return str(value) match = RE_SCHEMA_ELEMENT.match(typ)
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)
raise vol.Invalid("Fatal error for {}.".format(value)) # prepare range
except ValueError: range_args = {}
raise vol.Invalid( for group_name in ('i_min', 'i_max', 'f_min', 'f_max'):
"Type {} error for {}.".format(typ, value)) from None 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))
def _nested_validate(typ, data_list): def _nested_validate_list(typ, data_list, key):
"""Validate nested items.""" """Validate nested items."""
options = [] options = []
for element in data_list: for element in data_list:
# dict list # Nested?
if isinstance(typ, dict): if isinstance(typ, dict):
c_options = {} c_options = _nested_validate_dict(typ, element, key)
for c_key, c_value in element.items():
if c_key not in typ:
raise vol.Invalid(
"Unknown nested options {}.".format(c_key))
c_options[c_key] = _single_validate(typ[c_key], c_value)
options.append(c_options) options.append(c_options)
# normal list
else: else:
options.append(_single_validate(typ, element)) options.append(_single_validate(typ, element, key))
return options return options
def _nested_validate_dict(typ, data_dict, key):
"""Validate nested items."""
options = {}
for c_key, c_value in data_dict.items():
# Ignore unknown options / remove from list
if c_key not in typ:
_LOGGER.warning("Unknown options %s", c_key)
continue
# Nested?
if isinstance(typ[c_key], list):
options[c_key] = _nested_validate_list(typ[c_key][0],
c_value, c_key)
else:
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
_check_missing_options(typ, options, key)
return options
def _check_missing_options(origin, exists, root):
"""Check if all options are exists."""
missing = set(origin) - set(exists)
for miss_opt in missing:
if isinstance(origin[miss_opt], str) and \
origin[miss_opt].endswith("?"):
continue
raise vol.Invalid(
"Missing option {} in {}".format(miss_opt, root))

View File

@@ -1,5 +1,6 @@
"""Init file for HassIO rest api.""" """Init file for HassIO rest api."""
import logging import logging
from pathlib import Path
from aiohttp import web from aiohttp import web
@@ -8,6 +9,8 @@ from .homeassistant import APIHomeAssistant
from .host import APIHost from .host import APIHost
from .network import APINetwork from .network import APINetwork
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .security import APISecurity
from .snapshots import APISnapshots
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -25,14 +28,16 @@ class RestAPI(object):
self._handler = None self._handler = None
self.server = None self.server = None
def register_host(self, host_control): def register_host(self, host_control, hardware):
"""Register hostcontrol function.""" """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/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/reboot', api_host.reboot)
self.webapp.router.add_post('/host/shutdown', api_host.shutdown) 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/update', api_host.update)
self.webapp.router.add_post('/host/options', api_host.options)
def register_network(self, host_control): def register_network(self, host_control):
"""Register network function.""" """Register network function."""
@@ -41,15 +46,15 @@ class RestAPI(object):
self.webapp.router.add_get('/network/info', api_net.info) self.webapp.router.add_get('/network/info', api_net.info)
self.webapp.router.add_post('/network/options', api_net.options) self.webapp.router.add_post('/network/options', api_net.options)
def register_supervisor(self, supervisor, addons, host_control): def register_supervisor(self, supervisor, snapshots, addons, host_control,
updater):
"""Register supervisor function.""" """Register supervisor function."""
api_supervisor = APISupervisor( api_supervisor = APISupervisor(
self.config, self.loop, supervisor, addons, host_control) self.config, self.loop, supervisor, snapshots, addons,
host_control, updater)
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
self.webapp.router.add_get('/supervisor/info', api_supervisor.info) self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
self.webapp.router.add_get(
'/supervisor/addons', api_supervisor.available_addons)
self.webapp.router.add_post( self.webapp.router.add_post(
'/supervisor/update', api_supervisor.update) '/supervisor/update', api_supervisor.update)
self.webapp.router.add_post( self.webapp.router.add_post(
@@ -63,13 +68,25 @@ class RestAPI(object):
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant) api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
self.webapp.router.add_get('/homeassistant/info', api_hass.info) self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs) self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
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_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)
self.webapp.router.add_post(
'/homeassistant/api/{path:.+}', api_hass.api)
self.webapp.router.add_get(
'/homeassistant/api/{path:.+}', api_hass.api)
def register_addons(self, addons): def register_addons(self, addons):
"""Register homeassistant function.""" """Register homeassistant function."""
api_addons = APIAddons(self.config, self.loop, addons) 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_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_post( self.webapp.router.add_post(
'/addons/{addon}/install', api_addons.install) '/addons/{addon}/install', api_addons.install)
@@ -77,11 +94,63 @@ class RestAPI(object):
'/addons/{addon}/uninstall', api_addons.uninstall) '/addons/{addon}/uninstall', api_addons.uninstall)
self.webapp.router.add_post('/addons/{addon}/start', api_addons.start) self.webapp.router.add_post('/addons/{addon}/start', api_addons.start)
self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop) self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop)
self.webapp.router.add_post(
'/addons/{addon}/restart', api_addons.restart)
self.webapp.router.add_post( self.webapp.router.add_post(
'/addons/{addon}/update', api_addons.update) '/addons/{addon}/update', api_addons.update)
self.webapp.router.add_post( self.webapp.router.add_post(
'/addons/{addon}/options', api_addons.options) '/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}/logs', api_addons.logs)
self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo)
self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin)
def register_security(self):
"""Register security function."""
api_security = APISecurity(self.config, self.loop)
self.webapp.router.add_get('/security/info', api_security.info)
self.webapp.router.add_post('/security/options', api_security.options)
self.webapp.router.add_post('/security/totp', api_security.totp)
self.webapp.router.add_post('/security/session', api_security.session)
def register_snapshots(self, snapshots):
"""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(
'/snapshots/new/partial', api_snapshots.snapshot_partial)
self.webapp.router.add_get(
'/snapshots/{snapshot}/info', api_snapshots.info)
self.webapp.router.add_post(
'/snapshots/{snapshot}/remove', api_snapshots.remove)
self.webapp.router.add_post(
'/snapshots/{snapshot}/restore/full', api_snapshots.restore_full)
self.webapp.router.add_post(
'/snapshots/{snapshot}/restore/partial',
api_snapshots.restore_partial)
def register_panel(self):
"""Register panel for homeassistant."""
def create_panel_response(build_type):
"""Create a function to generate a response."""
path = Path(__file__).parents[1].joinpath(
'panel/hassio-main-{}.html'.format(build_type))
return lambda request: web.FileResponse(path)
# This route is for backwards compatibility with HA < 0.58
self.webapp.router.add_get('/panel', create_panel_response('es5'))
self.webapp.router.add_get('/panel_es5', create_panel_response('es5'))
self.webapp.router.add_get(
'/panel_latest', create_panel_response('latest'))
async def start(self): async def start(self):
"""Run rest api webserver.""" """Run rest api webserver."""
@@ -102,5 +171,5 @@ class RestAPI(object):
await self.webapp.shutdown() await self.webapp.shutdown()
if self._handler: if self._handler:
await self._handler.finish_connections(60) await self._handler.shutdown(60)
await self.webapp.cleanup() await self.webapp.cleanup()

View File

@@ -8,7 +8,14 @@ from voluptuous.humanize import humanize_error
from .util import api_process, api_process_raw, api_validate from .util import api_process, api_process_raw, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
ATTR_URL, STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL) ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
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,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY)
from ..validate import DOCKER_PORTS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -16,8 +23,11 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_VERSION): vol.Coerce(str),
}) })
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]) vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): vol.Any(None, DOCKER_PORTS),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
}) })
@@ -32,114 +42,208 @@ class APIAddons(object):
def _extract_addon(self, request, check_installed=True): def _extract_addon(self, request, check_installed=True):
"""Return addon and if not exists trow a exception.""" """Return addon and if not exists trow a exception."""
addon = request.match_info.get('addon') addon = self.addons.get(request.match_info.get('addon'))
if not addon:
# check data
if not self.addons.exists_addon(addon):
raise RuntimeError("Addon not exists") raise RuntimeError("Addon not exists")
if check_installed and not self.addons.is_installed(addon):
if check_installed and not addon.is_installed:
raise RuntimeError("Addon is not installed") raise RuntimeError("Addon is not installed")
return addon return addon
@staticmethod
def _pretty_devices(addon):
"""Return a simplified device list."""
dev_list = addon.devices
if not dev_list:
return
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_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_AUDIO: addon.with_audio,
ATTR_GPIO: addon.with_gpio,
})
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 @api_process
async def info(self, request): async def info(self, request):
"""Return addon information.""" """Return addon information."""
addon = self._extract_addon(request) addon = self._extract_addon(request, check_installed=False)
return { return {
ATTR_VERSION: self.addons.version_installed(addon), ATTR_NAME: addon.name,
ATTR_LAST_VERSION: self.addons.get_last_version(addon), ATTR_DESCRIPTON: addon.description,
ATTR_STATE: await self.addons.state(addon), ATTR_VERSION: addon.version_installed,
ATTR_BOOT: self.addons.get_boot(addon), ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_OPTIONS: self.addons.get_options(addon), ATTR_REPOSITORY: addon.repository,
ATTR_URL: self.addons.get_url(addon), ATTR_LAST_VERSION: addon.last_version,
ATTR_STATE: await addon.state(),
ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options,
ATTR_URL: addon.url,
ATTR_DETACHED: addon.is_detached,
ATTR_BUILD: addon.need_build,
ATTR_NETWORK: addon.ports,
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_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio,
ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
} }
@api_process @api_process
async def options(self, request): async def options(self, request):
"""Store user options for addon.""" """Store user options for addon."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
options_schema = self.addons.get_schema(addon)
addon_schema = SCHEMA_OPTIONS.extend({ addon_schema = SCHEMA_OPTIONS.extend({
vol.Optional(ATTR_OPTIONS): options_schema, vol.Optional(ATTR_OPTIONS): addon.schema,
}) })
body = await api_validate(addon_schema, request) body = await api_validate(addon_schema, request)
if ATTR_OPTIONS in body: if ATTR_OPTIONS in body:
self.addons.set_options(addon, body[ATTR_OPTIONS]) addon.options = body[ATTR_OPTIONS]
if ATTR_BOOT in body: if ATTR_BOOT in body:
self.addons.set_boot(addon, body[ATTR_BOOT]) addon.boot = body[ATTR_BOOT]
if ATTR_AUTO_UPDATE in body:
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 return True
@api_process @api_process
async def install(self, request): def install(self, request):
"""Install addon.""" """Install addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
version = body.get( return asyncio.shield(addon.install(), loop=self.loop)
ATTR_VERSION, self.addons.get_last_version(addon))
return await asyncio.shield(
self.addons.install(addon, version), loop=self.loop)
@api_process @api_process
async def uninstall(self, request): def uninstall(self, request):
"""Uninstall addon.""" """Uninstall addon."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return asyncio.shield(addon.uninstall(), loop=self.loop)
return await asyncio.shield(
self.addons.uninstall(addon), loop=self.loop)
@api_process @api_process
async def start(self, request): def start(self, request):
"""Start addon.""" """Start addon."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
if await self.addons.state(addon) == STATE_STARTED: # check options
raise RuntimeError("Addon is already running") options = addon.options
# validate options
try: try:
schema = self.addons.get_schema(addon) addon.schema(options)
options = self.addons.get_options(addon)
schema(options)
except vol.Invalid as ex: except vol.Invalid as ex:
raise RuntimeError(humanize_error(options, ex)) from None raise RuntimeError(humanize_error(options, ex)) from None
return await asyncio.shield( return asyncio.shield(addon.start(), loop=self.loop)
self.addons.start(addon), loop=self.loop)
@api_process @api_process
async def stop(self, request): def stop(self, request):
"""Stop addon.""" """Stop addon."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return asyncio.shield(addon.stop(), loop=self.loop)
if await self.addons.state(addon) == STATE_STOPPED:
raise RuntimeError("Addon is already stoped")
return await asyncio.shield(
self.addons.stop(addon), loop=self.loop)
@api_process @api_process
async def update(self, request): def update(self, request):
"""Update addon.""" """Update addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request) addon = self._extract_addon(request)
version = body.get(
ATTR_VERSION, self.addons.get_last_version(addon))
if version == self.addons.version_installed(addon): if addon.last_version == addon.version_installed:
raise RuntimeError("Version is already in use") raise RuntimeError("No update available!")
return await asyncio.shield( return asyncio.shield(addon.update(), loop=self.loop)
self.addons.update(addon, version), loop=self.loop)
@api_process_raw @api_process
def restart(self, request):
"""Restart addon."""
addon = self._extract_addon(request)
return asyncio.shield(addon.restart(), loop=self.loop)
@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): def logs(self, request):
"""Return logs from addon.""" """Return logs from addon."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return self.addons.logs(addon) 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()
@api_process
async def stdin(self, request):
"""Write to stdin of addon."""
addon = self._extract_addon(request)
if not addon.with_stdin:
raise RuntimeError("STDIN not supported by addons")
data = await request.read()
return await asyncio.shield(addon.write_stdin(data), loop=self.loop)

View File

@@ -2,13 +2,36 @@
import asyncio import asyncio
import logging import logging
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
import voluptuous as vol import voluptuous as vol
from .util import api_process, api_process_raw, api_validate from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
ATTR_BOOT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG,
CONTENT_TYPE_BINARY, HEADER_HA_ACCESS)
from ..validate import HASS_DEVICES, NETWORK_PORT
_LOGGER = logging.getLogger(__name__) _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)),
vol.Optional(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
})
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_VERSION): vol.Coerce(str),
}) })
@@ -23,35 +46,137 @@ class APIHomeAssistant(object):
self.loop = loop self.loop = loop
self.homeassistant = homeassistant self.homeassistant = homeassistant
async def homeassistant_proxy(self, path, request):
"""Return a client request with proxy origin for Home-Assistant."""
url = "{}/api/{}".format(self.homeassistant.api_url, path)
try:
data = None
headers = {}
method = getattr(
self.homeassistant.websession, request.method.lower())
# read data
with async_timeout.timeout(10, loop=self.loop):
data = await request.read()
if data:
headers.update({CONTENT_TYPE: request.content_type})
# need api password?
if self.homeassistant.api_password:
headers = {HEADER_HA_ACCESS: self.homeassistant.api_password}
# reset headers
if not headers:
headers = None
client = await method(
url, data=data, headers=headers, timeout=300
)
return client
except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s.", path, err)
except asyncio.TimeoutError:
_LOGGER.error("Client timeout error on api request %s.", path)
raise HTTPBadGateway()
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return host information.""" """Return host information."""
info = { return {
ATTR_VERSION: self.homeassistant.version, ATTR_VERSION: self.homeassistant.version,
ATTR_LAST_VERSION: self.config.last_homeassistant, 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,
ATTR_PORT: self.homeassistant.api_port,
ATTR_SSL: self.homeassistant.api_ssl,
ATTR_WATCHDOG: self.homeassistant.watchdog,
} }
return info @api_process
async def options(self, request):
"""Set homeassistant options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DEVICES in body:
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]
if ATTR_PORT in body:
self.homeassistant.api_port = body[ATTR_PORT]
if ATTR_PASSWORD in body:
self.homeassistant.api_password = body[ATTR_PASSWORD]
if ATTR_SSL in body:
self.homeassistant.api_ssl = body[ATTR_SSL]
if ATTR_WATCHDOG in body:
self.homeassistant.watchdog = body[ATTR_WATCHDOG]
return True
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update host OS.""" """Update homeassistant."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.last_homeassistant) version = body.get(ATTR_VERSION, self.homeassistant.last_version)
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
if version == self.homeassistant.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( return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop) self.homeassistant.update(version), loop=self.loop)
@api_process_raw @api_process
def logs(self, request): def stop(self, request):
"""Return homeassistant docker logs. """Stop homeassistant."""
return asyncio.shield(self.homeassistant.stop(), loop=self.loop)
Return a coroutine. @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."""
return asyncio.shield(self.homeassistant.restart(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return homeassistant docker logs."""
return self.homeassistant.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
async def api(self, request):
"""Proxy API request to Home-Assistant."""
path = request.match_info.get('path')
client = await self.homeassistant_proxy(path, request)
return web.Response(
body=await client.read(),
status=client.status,
content_type=client.content_type
)

View File

@@ -7,7 +7,9 @@ import voluptuous as vol
from .util import api_process_hostcontrol, api_process, api_validate from .util import api_process_hostcontrol, api_process, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES, 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, ATTR_GPIO)
from ..validate import ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -15,15 +17,21 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), 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): class APIHost(object):
"""Handle rest api for host functions.""" """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.""" """Initialize host rest api part."""
self.config = config self.config = config
self.loop = loop self.loop = loop
self.host_control = host_control self.host_control = host_control
self.local_hw = hardware
@api_process @api_process
async def info(self, request): async def info(self, request):
@@ -37,6 +45,18 @@ class APIHost(object):
ATTR_OS: self.host_control.os_info, 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 @api_process_hostcontrol
def reboot(self, request): def reboot(self, request):
"""Reboot host.""" """Reboot host."""
@@ -54,7 +74,18 @@ class APIHost(object):
version = body.get(ATTR_VERSION, self.host_control.last_version) version = body.get(ATTR_VERSION, self.host_control.last_version)
if version == self.host_control.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( return await asyncio.shield(
self.host_control.update(version=version), loop=self.loop) self.host_control.update(version=version), loop=self.loop)
@api_process
async def hardware(self, request):
"""Return local hardware infos."""
return {
ATTR_SERIAL: list(self.local_hw.serial_devices),
ATTR_INPUT: list(self.local_hw.input_devices),
ATTR_DISK: list(self.local_hw.disk_devices),
ATTR_GPIO: list(self.local_hw.gpio_devices),
ATTR_AUDIO: self.local_hw.audio_devices,
}

View File

@@ -1,11 +1,19 @@
"""Init file for HassIO network rest api.""" """Init file for HassIO network rest api."""
import logging import logging
from .util import api_process_hostcontrol import voluptuous as vol
from .util import api_process, api_process_hostcontrol, api_validate
from ..const import ATTR_HOSTNAME
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_HOSTNAME): vol.Coerce(str),
})
class APINetwork(object): class APINetwork(object):
"""Handle rest api for network functions.""" """Handle rest api for network functions."""
@@ -15,12 +23,21 @@ class APINetwork(object):
self.loop = loop self.loop = loop
self.host_control = host_control self.host_control = host_control
@api_process_hostcontrol @api_process
def info(self, request): async def info(self, request):
"""Show network settings.""" """Show network settings."""
pass return {
ATTR_HOSTNAME: self.host_control.hostname,
}
@api_process_hostcontrol @api_process_hostcontrol
def options(self, request): async def options(self, request):
"""Edit network settings.""" """Edit network settings."""
pass body = await api_validate(SCHEMA_OPTIONS, request)
# hostname
if ATTR_HOSTNAME in body:
if self.host_control.hostname != body[ATTR_HOSTNAME]:
await self.host_control.set_hostname(body[ATTR_HOSTNAME])
return True

102
hassio/api/security.py Normal file
View File

@@ -0,0 +1,102 @@
"""Init file for HassIO security rest api."""
from datetime import datetime, timedelta
import io
import logging
import hashlib
import os
from aiohttp import web
import voluptuous as vol
import pyotp
import pyqrcode
from .util import api_process, api_validate, hash_password
from ..const import ATTR_INITIALIZE, ATTR_PASSWORD, ATTR_TOTP, ATTR_SESSION
_LOGGER = logging.getLogger(__name__)
SCHEMA_PASSWORD = vol.Schema({
vol.Required(ATTR_PASSWORD): vol.Coerce(str),
})
SCHEMA_SESSION = SCHEMA_PASSWORD.extend({
vol.Optional(ATTR_TOTP, default=None): vol.Coerce(str),
})
class APISecurity(object):
"""Handle rest api for security functions."""
def __init__(self, config, loop):
"""Initialize security rest api part."""
self.config = config
self.loop = loop
def _check_password(self, body):
"""Check if password is valid and security is initialize."""
if not self.config.security_initialize:
raise RuntimeError("First set a password")
password = hash_password(body[ATTR_PASSWORD])
if password != self.config.security_password:
raise RuntimeError("Wrong password")
@api_process
async def info(self, request):
"""Return host information."""
return {
ATTR_INITIALIZE: self.config.security_initialize,
ATTR_TOTP: self.config.security_totp is not None,
}
@api_process
async def options(self, request):
"""Set options / password."""
body = await api_validate(SCHEMA_PASSWORD, request)
if self.config.security_initialize:
raise RuntimeError("Password is already set!")
self.config.security_password = hash_password(body[ATTR_PASSWORD])
self.config.security_initialize = True
return True
@api_process
async def totp(self, request):
"""Set and initialze TOTP."""
body = await api_validate(SCHEMA_PASSWORD, request)
self._check_password(body)
# generate TOTP
totp_init_key = pyotp.random_base32()
totp = pyotp.TOTP(totp_init_key)
# init qrcode
buff = io.BytesIO()
qrcode = pyqrcode.create(totp.provisioning_uri("Hass.IO"))
qrcode.svg(buff)
# finish
self.config.security_totp = totp_init_key
return web.Response(body=buff.getvalue(), content_type='image/svg+xml')
@api_process
async def session(self, request):
"""Set and initialze session."""
body = await api_validate(SCHEMA_SESSION, request)
self._check_password(body)
# check TOTP
if self.config.security_totp:
totp = pyotp.TOTP(self.config.security_totp)
if body[ATTR_TOTP] != totp.now():
raise RuntimeError("Invalid TOTP token!")
# create session
valid_until = datetime.now() + timedelta(days=1)
session = hashlib.sha256(os.urandom(54)).hexdigest()
# store session
self.config.add_security_session(session, valid_until)
return {ATTR_SESSION: session}

135
hassio/api/snapshots.py Normal file
View File

@@ -0,0 +1,135 @@
"""Init file for HassIO snapshot rest api."""
import asyncio
import logging
import voluptuous as vol
from .util import api_process, api_validate
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_SNAPSHOTS)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_RESTORE_PARTIAL = vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
vol.Optional(ATTR_ADDONS): [vol.Coerce(str)],
vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)],
})
SCHEMA_SNAPSHOT_FULL = vol.Schema({
vol.Optional(ATTR_NAME): vol.Coerce(str),
})
SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
vol.Optional(ATTR_ADDONS): [vol.Coerce(str)],
vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)],
})
class APISnapshots(object):
"""Handle rest api for snapshot functions."""
def __init__(self, config, loop, snapshots):
"""Initialize network rest api part."""
self.config = config
self.loop = loop
self.snapshots = snapshots
def _extract_snapshot(self, request):
"""Return addon and if not exists trow a exception."""
snapshot = self.snapshots.get(request.match_info.get('snapshot'))
if not snapshot:
raise RuntimeError("Snapshot not exists")
return snapshot
@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 {
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,
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
ATTR_SIZE: snapshot.size,
ATTR_HOMEASSISTANT: {
ATTR_VERSION: snapshot.homeassistant_version,
ATTR_DEVICES: snapshot.homeassistant_devices,
},
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: snapshot.repositories,
ATTR_FOLDERS: snapshot.folders,
}
@api_process
async def snapshot_full(self, request):
"""Full-Snapshot a snapshot."""
body = await api_validate(SCHEMA_SNAPSHOT_FULL, request)
return await asyncio.shield(
self.snapshots.do_snapshot_full(**body), loop=self.loop)
@api_process
async def snapshot_partial(self, request):
"""Partial-Snapshot a snapshot."""
body = await api_validate(SCHEMA_SNAPSHOT_PARTIAL, request)
return await asyncio.shield(
self.snapshots.do_snapshot_partial(**body), loop=self.loop)
@api_process
def restore_full(self, request):
"""Full-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
return asyncio.shield(
self.snapshots.do_restore_full(snapshot), loop=self.loop)
@api_process
async def restore_partial(self, request):
"""Partial-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
body = await api_validate(SCHEMA_SNAPSHOT_PARTIAL, request)
return await asyncio.shield(
self.snapshots.do_restore_partial(snapshot, **body),
loop=self.loop
)
@api_process
async def remove(self, request):
"""Remove a snapshot."""
snapshot = self._extract_snapshot(request)
return self.snapshots.remove(snapshot)

View File

@@ -5,12 +5,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from .util import api_process, api_process_raw, api_validate from .util import api_process, api_process_raw, api_validate
from ..addons.util import create_hash_index_list
from ..const import ( from ..const import (
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH,
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES, HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY,
ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE,
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL) ATTR_STATE, CONTENT_TYPE_BINARY)
from ..validate import validate_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -18,6 +18,7 @@ SCHEMA_OPTIONS = vol.Schema({
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(), vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(),
vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()], vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()],
vol.Optional(ATTR_TIMEZONE): validate_timezone,
}) })
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({
@@ -28,54 +29,16 @@ SCHEMA_VERSION = vol.Schema({
class APISupervisor(object): class APISupervisor(object):
"""Handle rest api for supervisor functions.""" """Handle rest api for supervisor functions."""
def __init__(self, config, loop, supervisor, addons, host_control): def __init__(self, config, loop, supervisor, snapshots, addons,
host_control, updater):
"""Initialize supervisor rest api part.""" """Initialize supervisor rest api part."""
self.config = config self.config = config
self.loop = loop self.loop = loop
self.supervisor = supervisor self.supervisor = supervisor
self.addons = addons self.addons = addons
self.snapshots = snapshots
self.host_control = host_control self.host_control = host_control
self.updater = updater
def _addons_list(self, only_installed=False):
"""Return a list of addons."""
detached = self.addons.list_detached
if only_installed:
addons = self.addons.data_installed
else:
addons = self.addons.data_all
data = []
for addon, values in addons.items():
i_version = self.addons.version_installed(addon)
data.append({
ATTR_NAME: values[ATTR_NAME],
ATTR_SLUG: addon,
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
ATTR_VERSION: values[ATTR_VERSION],
ATTR_INSTALLED: i_version,
ATTR_DETACHED: addon in detached,
ATTR_REPOSITORY: values[ATTR_REPOSITORY],
})
return data
def _repositories_list(self):
"""Return a list of addons repositories."""
data = []
list_id = create_hash_index_list(self.config.addons_repositories)
for repository in self.addons.list_repositories:
data.append({
ATTR_SLUG: repository[ATTR_SLUG],
ATTR_NAME: repository[ATTR_NAME],
ATTR_SOURCE: list_id.get(repository[ATTR_SLUG]),
ATTR_URL: repository.get(ATTR_URL),
ATTR_MAINTAINER: repository.get(ATTR_MAINTAINER),
})
return data
@api_process @api_process
async def ping(self, request): async def ping(self, request):
@@ -85,47 +48,44 @@ class APISupervisor(object):
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return host information.""" """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 { return {
ATTR_VERSION: HASSIO_VERSION, ATTR_VERSION: HASSIO_VERSION,
ATTR_LAST_VERSION: self.config.last_hassio, ATTR_LAST_VERSION: self.updater.version_hassio,
ATTR_BETA_CHANNEL: self.config.upstream_beta, ATTR_BETA_CHANNEL: self.updater.beta_channel,
ATTR_ADDONS: self._addons_list(only_installed=True), ATTR_ARCH: self.config.arch,
ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: list_addons,
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories, ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
} }
@api_process
async def available_addons(self, request):
"""Return information for all available addons."""
return {
ATTR_ADDONS: self._addons_list(),
ATTR_REPOSITORIES: self._repositories_list(),
}
@api_process @api_process
async def options(self, request): async def options(self, request):
"""Set supervisor options.""" """Set supervisor options."""
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_BETA_CHANNEL in body: 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]
if ATTR_ADDONS_REPOSITORIES in body: if ATTR_ADDONS_REPOSITORIES in body:
new = set(body[ATTR_ADDONS_REPOSITORIES]) new = set(body[ATTR_ADDONS_REPOSITORIES])
old = set(self.config.addons_repositories) await asyncio.shield(self.addons.load_repositories(new))
# add new repositories
tasks = [self.addons.add_git_repository(url) for url in
set(new - old)]
if tasks:
await asyncio.shield(
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
# remove old repositories
for url in set(old - new):
self.addons.drop_git_repository(url)
# read repository
self.addons.read_data_from_repositories()
return True return True
@@ -133,10 +93,10 @@ class APISupervisor(object):
async def update(self, request): async def update(self, request):
"""Update supervisor OS.""" """Update supervisor OS."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.last_hassio) version = body.get(ATTR_VERSION, self.updater.version_hassio)
if version == self.supervisor.version: if version == self.supervisor.version:
raise RuntimeError("Version is already in use") raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield( return await asyncio.shield(
self.supervisor.update(version), loop=self.loop) self.supervisor.update(version), loop=self.loop)
@@ -145,7 +105,9 @@ class APISupervisor(object):
async def reload(self, request): async def reload(self, request):
"""Reload addons, config ect.""" """Reload addons, config ect."""
tasks = [ tasks = [
self.addons.reload(), self.config.fetch_update_infos(), self.addons.reload(),
self.snapshots.reload(),
self.updater.fetch_data(),
self.host_control.load() self.host_control.load()
] ]
results, _ = await asyncio.shield( results, _ = await asyncio.shield(
@@ -157,10 +119,7 @@ class APISupervisor(object):
return True return True
@api_process_raw @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return supervisor docker logs. """Return supervisor docker logs."""
Return a coroutine.
"""
return self.supervisor.logs() return self.supervisor.logs()

View File

@@ -1,5 +1,6 @@
"""Init file for HassIO util for rest api.""" """Init file for HassIO util for rest api."""
import json import json
import hashlib
import logging import logging
from aiohttp import web from aiohttp import web
@@ -8,7 +9,8 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from ..const import ( from ..const import (
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR) JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
CONTENT_TYPE_BINARY)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -32,6 +34,8 @@ def api_process(method):
if isinstance(answer, dict): if isinstance(answer, dict):
return api_return_ok(data=answer) return api_return_ok(data=answer)
if isinstance(answer, web.Response):
return answer
elif answer: elif answer:
return api_return_ok() return api_return_ok()
return api_return_error() return api_return_error()
@@ -62,18 +66,23 @@ def api_process_hostcontrol(method):
return wrap_hostcontrol return wrap_hostcontrol
def api_process_raw(method): def api_process_raw(content):
"""Wrap function with raw output to rest api.""" """Wrap content_type into function."""
async def wrap_api(api, *args, **kwargs): def wrap_method(method):
"""Return api information.""" """Wrap function with raw output to rest api."""
try: async def wrap_api(api, *args, **kwargs):
message = await method(api, *args, **kwargs) """Return api information."""
except RuntimeError as err: try:
message = str(err).encode() 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): def api_return_error(message=None):
@@ -101,3 +110,9 @@ async def api_validate(schema, request):
raise RuntimeError(humanize_error(data, ex)) from None raise RuntimeError(humanize_error(data, ex)) from None
return data return data
def hash_password(password):
"""Hash and salt our passwords."""
key = ")*()*SALT_HASSIO2123{}6554547485HSKA!!*JSLAfdasda$".format(password)
return hashlib.sha256(key.encode()).hexdigest()

View File

@@ -2,6 +2,8 @@
import logging import logging
import os import os
import signal import signal
import shutil
from pathlib import Path
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
@@ -11,9 +13,9 @@ from .config import CoreConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def initialize_system_data(websession): def initialize_system_data():
"""Setup default config and create folders.""" """Setup default config and create folders."""
config = CoreConfig(websession) config = CoreConfig()
# homeassistant config folder # homeassistant config folder
if not config.path_config.is_dir(): if not config.path_config.is_dir():
@@ -21,36 +23,57 @@ def initialize_system_data(websession):
"Create Home-Assistant config folder %s", config.path_config) "Create Home-Assistant config folder %s", config.path_config)
config.path_config.mkdir() config.path_config.mkdir()
# homeassistant ssl folder # hassio ssl folder
if not config.path_ssl.is_dir(): if not config.path_ssl.is_dir():
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl) _LOGGER.info("Create hassio ssl folder %s", config.path_ssl)
config.path_ssl.mkdir() config.path_ssl.mkdir()
# homeassistant addon data folder # hassio addon data folder
if not config.path_addons_data.is_dir(): if not config.path_addons_data.is_dir():
_LOGGER.info("Create Home-Assistant addon data folder %s", _LOGGER.info(
config.path_addons_data) "Create hassio addon data folder %s", config.path_addons_data)
config.path_addons_data.mkdir(parents=True) config.path_addons_data.mkdir(parents=True)
if not config.path_addons_local.is_dir(): if not config.path_addons_local.is_dir():
_LOGGER.info("Create Home-Assistant addon local repository folder %s", _LOGGER.info("Create hassio addon local repository folder %s",
config.path_addons_local) config.path_addons_local)
config.path_addons_local.mkdir(parents=True) config.path_addons_local.mkdir(parents=True)
if not config.path_addons_git.is_dir(): if not config.path_addons_git.is_dir():
_LOGGER.info("Create Home-Assistant addon git repositories folder %s", _LOGGER.info("Create hassio addon git repositories folder %s",
config.path_addons_git) config.path_addons_git)
config.path_addons_git.mkdir(parents=True) config.path_addons_git.mkdir(parents=True)
# homeassistant backup folder # hassio tmp folder
if not config.path_tmp.is_dir():
_LOGGER.info("Create hassio temp folder %s", config.path_tmp)
config.path_tmp.mkdir(parents=True)
# hassio backup folder
if not config.path_backup.is_dir(): if not config.path_backup.is_dir():
_LOGGER.info("Create Home-Assistant backup folder %s", _LOGGER.info("Create hassio backup folder %s", config.path_backup)
config.path_backup)
config.path_backup.mkdir() config.path_backup.mkdir()
# share folder
if not config.path_share.is_dir():
_LOGGER.info("Create hassio share folder %s", config.path_share)
config.path_share.mkdir()
return config return config
def migrate_system_env(config):
"""Cleanup some stuff after update."""
# hass.io 0.37 -> 0.38
old_build = Path(config.path_hassio, "addons/build")
if old_build.is_dir():
try:
old_build.rmdir()
except OSError:
_LOGGER.warning("Can't cleanup old addons build dir.")
def initialize_logging(): def initialize_logging():
"""Setup the logging.""" """Setup the logging."""
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -78,6 +101,7 @@ def initialize_logging():
def check_environment(): def check_environment():
"""Check if all environment are exists.""" """Check if all environment are exists."""
# check environment variables
for key in ('SUPERVISOR_SHARE', 'SUPERVISOR_NAME', for key in ('SUPERVISOR_SHARE', 'SUPERVISOR_NAME',
'HOMEASSISTANT_REPOSITORY'): 'HOMEASSISTANT_REPOSITORY'):
try: try:
@@ -86,29 +110,35 @@ def check_environment():
_LOGGER.fatal("Can't find %s in env!", key) _LOGGER.fatal("Can't find %s in env!", key)
return False return False
# check docker socket
if not SOCKET_DOCKER.is_socket(): if not SOCKET_DOCKER.is_socket():
_LOGGER.fatal("Can't find docker socket!") _LOGGER.fatal("Can't find docker socket!")
return False return False
# check socat exec
if not shutil.which('socat'):
_LOGGER.fatal("Can0t find socat program!")
return False
return True return True
def reg_signal(loop, hassio): def reg_signal(loop):
"""Register SIGTERM, SIGKILL to stop system.""" """Register SIGTERM, SIGKILL to stop system."""
try: try:
loop.add_signal_handler( loop.add_signal_handler(
signal.SIGTERM, lambda: loop.create_task(hassio.stop())) signal.SIGTERM, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGTERM") _LOGGER.warning("Could not bind to SIGTERM")
try: try:
loop.add_signal_handler( loop.add_signal_handler(
signal.SIGHUP, lambda: loop.create_task(hassio.stop())) signal.SIGHUP, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGHUP") _LOGGER.warning("Could not bind to SIGHUP")
try: try:
loop.add_signal_handler( loop.add_signal_handler(
signal.SIGINT, lambda: loop.create_task(hassio.stop())) signal.SIGINT, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGINT") _LOGGER.warning("Could not bind to SIGINT")

View File

@@ -1,153 +1,73 @@
"""Bootstrap HassIO.""" """Bootstrap HassIO."""
from datetime import datetime
import logging import logging
import json
import os import os
from pathlib import Path, PurePath from pathlib import Path, PurePath
import voluptuous as vol from .const import (
from voluptuous.humanize import humanize_error FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS,
ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_LAST_BOOT)
from .tools import ( from .tools import JsonConfig, parse_datetime
fetch_last_versions, write_json_file, read_json_file) from .validate import SCHEMA_HASSIO_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HOMEASSISTANT_CONFIG = PurePath("homeassistant") HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last'
HASSIO_SSL = PurePath("ssl") HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last'
HASSIO_CLEANUP = 'hassio_cleanup'
ADDONS_CORE = PurePath("addons/core") ADDONS_CORE = PurePath("addons/core")
ADDONS_LOCAL = PurePath("addons/local") ADDONS_LOCAL = PurePath("addons/local")
ADDONS_GIT = PurePath("addons/git") ADDONS_GIT = PurePath("addons/git")
ADDONS_DATA = PurePath("addons/data") ADDONS_DATA = PurePath("addons/data")
ADDONS_CUSTOM_LIST = 'addons_custom_list'
BACKUP_DATA = PurePath("backup") BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp")
UPSTREAM_BETA = 'upstream_beta' DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
API_ENDPOINT = 'api_endpoint'
# pylint: disable=no-value-for-parameter class CoreConfig(JsonConfig):
SCHEMA_CONFIG = vol.Schema({
vol.Optional(UPSTREAM_BETA, default=False): vol.Boolean(),
vol.Optional(API_ENDPOINT): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(HASSIO_CLEANUP): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
}, extra=vol.REMOVE_EXTRA)
class Config(object):
"""Hold all config data."""
def __init__(self, config_file):
"""Initialize config object."""
self._file = config_file
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 = {}
def save(self):
"""Store data to config file."""
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
class CoreConfig(Config):
"""Hold all core config data.""" """Hold all core config data."""
def __init__(self, websession): def __init__(self):
"""Initialize config object.""" """Initialize config object."""
self.websession = websession super().__init__(FILE_HASSIO_CONFIG, SCHEMA_HASSIO_CONFIG)
self.arch = None
super().__init__(FILE_HASSIO_CONFIG)
# validate data
try:
self._data = SCHEMA_CONFIG(self._data)
self.save()
except vol.Invalid as ex:
_LOGGER.warning(
"Invalid config %s", humanize_error(self._data, ex))
async def fetch_update_infos(self):
"""Read current versions from web."""
last = await fetch_last_versions(
self.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 @property
def api_endpoint(self): def timezone(self):
"""Return IP address of api endpoint.""" """Return system timezone."""
return self._data[API_ENDPOINT] return self._data[ATTR_TIMEZONE]
@api_endpoint.setter @timezone.setter
def api_endpoint(self, value): def timezone(self, value):
"""Store IP address of api endpoint.""" """Set system timezone."""
self._data[API_ENDPOINT] = value self._data[ATTR_TIMEZONE] = 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)
@property
def hassio_cleanup(self):
"""Return Version they need to cleanup."""
return self._data.get(HASSIO_CLEANUP)
@hassio_cleanup.setter
def hassio_cleanup(self, version):
"""Set or remove cleanup flag."""
if version is None:
self._data.pop(HASSIO_CLEANUP, None)
else:
self._data[HASSIO_CLEANUP] = version
self.save() self.save()
@property @property
def homeassistant_image(self): def last_boot(self):
"""Return docker homeassistant repository.""" """Return last boot datetime."""
return os.environ['HOMEASSISTANT_REPOSITORY'] boot_str = self._data.get(ATTR_LAST_BOOT, DEFAULT_BOOT_TIME)
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 @property
def last_homeassistant(self): def path_hassio(self):
"""Actual version of homeassistant.""" """Return hassio data path."""
return self._data.get(HOMEASSISTANT_LAST) return HASSIO_DATA
@property
def last_hassio(self):
"""Actual version of hassio."""
return self._data.get(HASSIO_LAST)
@property @property
def path_extern_hassio(self): def path_extern_hassio(self):
@@ -162,7 +82,7 @@ class CoreConfig(Config):
@property @property
def path_config(self): def path_config(self):
"""Return config path inside supervisor.""" """Return config path inside supervisor."""
return Path(HASSIO_SHARE, HOMEASSISTANT_CONFIG) return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG)
@property @property
def path_extern_ssl(self): def path_extern_ssl(self):
@@ -172,66 +92,155 @@ class CoreConfig(Config):
@property @property
def path_ssl(self): def path_ssl(self):
"""Return SSL path inside supervisor.""" """Return SSL path inside supervisor."""
return Path(HASSIO_SHARE, HASSIO_SSL) return Path(HASSIO_DATA, HASSIO_SSL)
@property @property
def path_addons_core(self): def path_addons_core(self):
"""Return git path for core addons.""" """Return git path for core addons."""
return Path(HASSIO_SHARE, ADDONS_CORE) return Path(HASSIO_DATA, ADDONS_CORE)
@property @property
def path_addons_git(self): def path_addons_git(self):
"""Return path for git addons.""" """Return path for git addons."""
return Path(HASSIO_SHARE, ADDONS_GIT) return Path(HASSIO_DATA, ADDONS_GIT)
@property @property
def path_addons_local(self): def path_addons_local(self):
"""Return path for customs addons.""" """Return path for customs addons."""
return Path(HASSIO_SHARE, ADDONS_LOCAL) return Path(HASSIO_DATA, ADDONS_LOCAL)
@property @property
def path_extern_addons_local(self): def path_extern_addons_local(self):
"""Return path for customs addons.""" """Return path for customs addons."""
return str(PurePath(self.path_extern_hassio, ADDONS_LOCAL)) return PurePath(self.path_extern_hassio, ADDONS_LOCAL)
@property @property
def path_addons_data(self): def path_addons_data(self):
"""Return root addon data folder.""" """Return root addon data folder."""
return Path(HASSIO_SHARE, ADDONS_DATA) return Path(HASSIO_DATA, ADDONS_DATA)
@property @property
def path_extern_addons_data(self): def path_extern_addons_data(self):
"""Return root addon data folder extern for docker.""" """Return root addon data folder extern for docker."""
return str(PurePath(self.path_extern_hassio, ADDONS_DATA)) return PurePath(self.path_extern_hassio, ADDONS_DATA)
@property
def path_tmp(self):
"""Return hass.io temp folder."""
return Path(HASSIO_DATA, TMP_DATA)
@property @property
def path_backup(self): def path_backup(self):
"""Return root backup data folder.""" """Return root backup data folder."""
return Path(HASSIO_SHARE, BACKUP_DATA) return Path(HASSIO_DATA, BACKUP_DATA)
@property @property
def path_extern_backup(self): def path_extern_backup(self):
"""Return root backup data folder extern for docker.""" """Return root backup data folder extern for docker."""
return str(PurePath(self.path_extern_hassio, BACKUP_DATA)) return PurePath(self.path_extern_hassio, BACKUP_DATA)
@property
def path_share(self):
"""Return root share data folder."""
return Path(HASSIO_DATA, SHARE_DATA)
@property
def path_extern_share(self):
"""Return root share data folder extern for docker."""
return PurePath(self.path_extern_hassio, SHARE_DATA)
@property @property
def addons_repositories(self): def addons_repositories(self):
"""Return list of addons custom repositories.""" """Return list of addons custom repositories."""
return self._data[ADDONS_CUSTOM_LIST] return self._data[ATTR_ADDONS_CUSTOM_LIST]
@addons_repositories.setter def add_addon_repository(self, repo):
def addons_repositories(self, repo):
"""Add a custom repository to list.""" """Add a custom repository to list."""
if repo in self._data[ADDONS_CUSTOM_LIST]: if repo in self._data[ATTR_ADDONS_CUSTOM_LIST]:
return return
self._data[ADDONS_CUSTOM_LIST].append(repo) self._data[ATTR_ADDONS_CUSTOM_LIST].append(repo)
self.save() self.save()
def drop_addon_repository(self, repo): def drop_addon_repository(self, repo):
"""Remove a custom repository from list.""" """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 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[ATTR_SECURITY]
@security_initialize.setter
def security_initialize(self, value):
"""Set is security initialize."""
self._data[ATTR_SECURITY] = value
self.save()
@property
def security_totp(self):
"""Return the TOTP key."""
return self._data.get(ATTR_TOTP)
@security_totp.setter
def security_totp(self, value):
"""Set the TOTP key."""
self._data[ATTR_TOTP] = value
self.save()
@property
def security_password(self):
"""Return the password key."""
return self._data.get(ATTR_PASSWORD)
@security_password.setter
def security_password(self, value):
"""Set the password key."""
self._data[ATTR_PASSWORD] = value
self.save()
@property
def security_sessions(self):
"""Return api sessions."""
return {
session: parse_datetime(until) for
session, until in self._data[ATTR_SESSIONS].items()
}
def add_security_session(self, session, valid):
"""Set the a new session."""
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() self.save()

View File

@@ -1,31 +1,45 @@
"""Const file for HassIO.""" """Const file for HassIO."""
from pathlib import Path from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '0.22' HASSIO_VERSION = '0.75'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/master/version.json') 'hassio/{}/version.json')
URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/dev/version.json')
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons' URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons'
DOCKER_REPO = "homeassistant" HASSIO_DATA = Path("/data")
HASSIO_SHARE = Path("/data")
RUN_UPDATE_INFO_TASKS = 28800 RUN_UPDATE_INFO_TASKS = 28800
RUN_UPDATE_SUPERVISOR_TASKS = 29100 RUN_UPDATE_SUPERVISOR_TASKS = 29100
RUN_UPDATE_ADDONS_TASKS = 57600
RUN_RELOAD_ADDONS_TASKS = 28800 RUN_RELOAD_ADDONS_TASKS = 28800
RUN_RELOAD_SNAPSHOTS_TASKS = 72000
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300
RUN_CLEANUP_API_SESSIONS = 900
RESTART_EXIT_CODE = 100 FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_ADDONS = Path(HASSIO_SHARE, "addons.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
FILE_HASSIO_CONFIG = Path(HASSIO_SHARE, "config.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.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'
META_ADDON = 'addon'
META_SUPERVISOR = 'supervisor'
META_HOMEASSISTANT = 'homeassistant'
JSON_RESULT = 'result' JSON_RESULT = 'result'
JSON_DATA = 'data' JSON_DATA = 'data'
JSON_MESSAGE = 'message' JSON_MESSAGE = 'message'
@@ -33,13 +47,24 @@ JSON_MESSAGE = 'message'
RESULT_ERROR = 'error' RESULT_ERROR = 'error'
RESULT_OK = 'ok' RESULT_OK = 'ok'
CONTENT_TYPE_BINARY = 'application/octet-stream'
CONTENT_TYPE_PNG = 'image/png'
CONTENT_TYPE_JSON = 'application/json'
HEADER_HA_ACCESS = 'x-ha-access'
ATTR_WATCHDOG = 'watchdog'
ATTR_DATE = 'date'
ATTR_ARCH = 'arch'
ATTR_HOSTNAME = 'hostname' ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone'
ATTR_ARGS = 'args'
ATTR_OS = 'os' ATTR_OS = 'os'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
ATTR_SOURCE = 'source' ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features' ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons' ATTR_ADDONS = 'addons'
ATTR_VERSION = 'version' ATTR_VERSION = 'version'
ATTR_LAST_BOOT = 'last_boot'
ATTR_LAST_VERSION = 'last_version' ATTR_LAST_VERSION = 'last_version'
ATTR_BETA_CHANNEL = 'beta_channel' ATTR_BETA_CHANNEL = 'beta_channel'
ATTR_NAME = 'name' ATTR_NAME = 'name'
@@ -48,21 +73,68 @@ ATTR_DESCRIPTON = 'description'
ATTR_STARTUP = 'startup' ATTR_STARTUP = 'startup'
ATTR_BOOT = 'boot' ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports' ATTR_PORTS = 'ports'
ATTR_PORT = 'port'
ATTR_SSL = 'ssl'
ATTR_MAP = 'map' ATTR_MAP = 'map'
ATTR_WEBUI = 'webui'
ATTR_OPTIONS = 'options' ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed' ATTR_INSTALLED = 'installed'
ATTR_DETACHED = 'detached' ATTR_DETACHED = 'detached'
ATTR_STATE = 'state' ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema' ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image' ATTR_IMAGE = 'image'
ATTR_LOGO = 'logo'
ATTR_STDIN = 'stdin'
ATTR_ADDONS_REPOSITORIES = 'addons_repositories' ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
ATTR_REPOSITORY = 'repository' ATTR_REPOSITORY = 'repository'
ATTR_REPOSITORIES = 'repositories' ATTR_REPOSITORIES = 'repositories'
ATTR_URL = 'url' ATTR_URL = 'url'
ATTR_MAINTAINER = 'maintainer' ATTR_MAINTAINER = 'maintainer'
ATTR_PASSWORD = 'password'
ATTR_TOTP = 'totp'
ATTR_INITIALIZE = 'initialize'
ATTR_SESSION = 'session'
ATTR_SESSIONS = 'sessions'
ATTR_LOCATON = 'location'
ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices'
ATTR_ENVIRONMENT = 'environment'
ATTR_HOST_NETWORK = 'host_network'
ATTR_NETWORK = 'network'
ATTR_TMPFS = 'tmpfs'
ATTR_PRIVILEGED = 'privileged'
ATTR_USER = 'user'
ATTR_SYSTEM = 'system'
ATTR_SNAPSHOTS = 'snapshots'
ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_HASSIO = 'hassio'
ATTR_HASSIO_API = 'hassio_api'
ATTR_HOMEASSISTANT_API = 'homeassistant_api'
ATTR_UUID = 'uuid'
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_BUILD_FROM = 'build_from'
ATTR_SQUASH = 'squash'
ATTR_GPIO = 'gpio'
ATTR_LEGACY = 'legacy'
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
STARTUP_BEFORE = 'before' STARTUP_INITIALIZE = 'initialize'
STARTUP_AFTER = 'after' STARTUP_SYSTEM = 'system'
STARTUP_SERVICES = 'services'
STARTUP_APPLICATION = 'application'
STARTUP_ONCE = 'once' STARTUP_ONCE = 'once'
BOOT_AUTO = 'auto' BOOT_AUTO = 'auto'
@@ -70,8 +142,26 @@ BOOT_MANUAL = 'manual'
STATE_STARTED = 'started' STATE_STARTED = 'started'
STATE_STOPPED = 'stopped' STATE_STOPPED = 'stopped'
STATE_NONE = 'none'
MAP_CONFIG = 'config' MAP_CONFIG = 'config'
MAP_SSL = 'ssl' MAP_SSL = 'ssl'
MAP_ADDONS = 'addons' MAP_ADDONS = 'addons'
MAP_BACKUP = 'backup' MAP_BACKUP = 'backup'
MAP_SHARE = 'share'
ARCH_ARMHF = 'armhf'
ARCH_AARCH64 = 'aarch64'
ARCH_AMD64 = 'amd64'
ARCH_I386 = 'i386'
REPOSITORY_CORE = 'core'
REPOSITORY_LOCAL = 'local'
FOLDER_HOMEASSISTANT = 'homeassistant'
FOLDER_SHARE = 'share'
FOLDER_ADDONS = 'addons/local'
FOLDER_SSL = 'ssl'
SNAPSHOT_FULL = 'full'
SNAPSHOT_PARTIAL = 'partial'

View File

@@ -3,19 +3,28 @@ import asyncio
import logging import logging
import aiohttp import aiohttp
import docker
from . import bootstrap
from .addons import AddonManager from .addons import AddonManager
from .api import RestAPI from .api import RestAPI
from .host_control import HostControl from .host_control import HostControl
from .const import ( 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, STARTUP_AFTER, STARTUP_BEFORE) RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT_DOCKER,
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 .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant from .dock import DockerAPI
from .dock.supervisor import DockerSupervisor from .dock.supervisor import DockerSupervisor
from .tools import get_arch_from_image, get_local_ip from .dns import DNSForward
from .snapshots import SnapshotsManager
from .updater import Updater
from .tasks import (
hassio_update, homeassistant_watchdog_docker, api_sessions_cleanup,
addons_update)
from .tools import fetch_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -23,37 +32,50 @@ _LOGGER = logging.getLogger(__name__)
class HassIO(object): class HassIO(object):
"""Main object of hassio.""" """Main object of hassio."""
def __init__(self, loop): def __init__(self, loop, config):
"""Initialize hassio object.""" """Initialize hassio object."""
self.exit_code = 0 self.exit_code = 0
self.loop = loop self.loop = loop
self.websession = aiohttp.ClientSession(loop=self.loop) self.config = config
self.config = bootstrap.initialize_system_data(self.websession) self.websession = aiohttp.ClientSession(loop=loop)
self.scheduler = Scheduler(self.loop) self.updater = Updater(config, loop, self.websession)
self.api = RestAPI(self.config, self.loop) self.scheduler = Scheduler(loop)
self.dock = docker.DockerClient( self.api = RestAPI(config, loop)
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto') self.hardware = Hardware()
self.docker = DockerAPI()
self.dns = DNSForward()
# init basic docker container # init basic docker container
self.supervisor = DockerSupervisor( self.supervisor = DockerSupervisor(
self.config, self.loop, self.dock, self) config, loop, self.docker, self.stop)
self.homeassistant = DockerHomeAssistant(
self.config, self.loop, self.dock) # init homeassistant
self.homeassistant = HomeAssistant(
config, loop, self.docker, self.updater)
# init HostControl # init HostControl
self.host_control = HostControl(self.loop) self.host_control = HostControl(loop)
# init addon system # init addon system
self.addons = AddonManager(self.config, self.loop, self.dock) self.addons = AddonManager(config, loop, self.docker)
# init snapshot system
self.snapshots = SnapshotsManager(
config, loop, self.scheduler, self.addons, self.homeassistant)
async def setup(self): async def setup(self):
"""Setup HassIO orchestration.""" """Setup HassIO orchestration."""
# supervisor # supervisor
await self.supervisor.attach() if not await self.supervisor.attach():
_LOGGER.fatal("Can't setup supervisor docker container!")
await self.supervisor.cleanup() await self.supervisor.cleanup()
# set api endpoint # set running arch
self.config.api_endpoint = await get_local_ip(self.loop) self.config.arch = self.supervisor.arch
# update timezone
if self.config.timezone == 'UTC':
self.config.timezone = await fetch_timezone(self.websession)
# hostcontrol # hostcontrol
await self.host_control.load() await self.host_control.load()
@@ -63,88 +85,106 @@ class HassIO(object):
self.host_control.load, RUN_UPDATE_INFO_TASKS) self.host_control.load, RUN_UPDATE_INFO_TASKS)
# rest api views # 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_network(self.host_control)
self.api.register_supervisor( self.api.register_supervisor(
self.supervisor, self.addons, self.host_control) self.supervisor, self.snapshots, self.addons, self.host_control,
self.updater)
self.api.register_homeassistant(self.homeassistant) self.api.register_homeassistant(self.homeassistant)
self.api.register_addons(self.addons) self.api.register_addons(self.addons)
self.api.register_security()
self.api.register_snapshots(self.snapshots)
self.api.register_panel()
# schedule update info tasks # schedule api session cleanup
self.scheduler.register_task( self.scheduler.register_task(
self.config.fetch_update_infos, RUN_UPDATE_INFO_TASKS, api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS,
now=True) now=True)
# first start of supervisor? # Load homeassistant
if not await self.homeassistant.exists(): await self.homeassistant.prepare()
_LOGGER.info("No HomeAssistant docker found.")
await self._setup_homeassistant()
# Load addons # Load addons
arch = get_arch_from_image(self.supervisor.image) await self.addons.prepare()
await self.addons.prepare(arch)
# schedule addon update task # schedule addon update task
self.scheduler.register_task( self.scheduler.register_task(
self.addons.reload, RUN_RELOAD_ADDONS_TASKS, now=True) self.addons.reload, RUN_RELOAD_ADDONS_TASKS, now=True)
self.scheduler.register_task(
addons_update(self.loop, self.addons), RUN_UPDATE_ADDONS_TASKS)
# schedule self update task # schedule self update task
self.scheduler.register_task( self.scheduler.register_task(
self._hassio_update, RUN_UPDATE_SUPERVISOR_TASKS) 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)
async def start(self): async def start(self):
"""Start HassIO orchestration.""" """Start HassIO orchestration."""
# on release channel, try update itself
# on beta channel, only read new versions
await asyncio.wait(
[hassio_update(self.supervisor, self.updater)()],
loop=self.loop
)
# start api # start api
await self.api.start() 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)
# HomeAssistant is already running / supervisor have only reboot try:
if await self.homeassistant.is_running(): # HomeAssistant is already running / supervisor have only reboot
_LOGGER.info("HassIO reboot detected") if self.hardware.last_boot == self.config.last_boot:
return _LOGGER.info("HassIO reboot detected")
return
# start addon mark as before # start addon mark as system
await self.addons.auto_boot(STARTUP_BEFORE) await self.addons.auto_boot(STARTUP_SYSTEM)
# run HomeAssistant # start addon mark as services
await self.homeassistant.run() await self.addons.auto_boot(STARTUP_SERVICES)
# start addon mark as after # run HomeAssistant
await self.addons.auto_boot(STARTUP_AFTER) if self.homeassistant.boot:
await self.homeassistant.run()
async def stop(self, exit_code=0): # 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
self.scheduler.register_task(
homeassistant_watchdog_docker(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT_DOCKER)
# self.scheduler.register_task(
# homeassistant_watchdog_api(self.loop, self.homeassistant),
# RUN_WATCHDOG_HOMEASSISTANT_API)
# If landingpage / run upgrade in background
if self.homeassistant.version == 'landingpage':
self.loop.create_task(self.homeassistant.install())
async def stop(self):
"""Stop a running orchestration.""" """Stop a running orchestration."""
# don't process scheduler anymore # don't process scheduler anymore
self.scheduler.stop() self.scheduler.suspend = True
# process stop task pararell # process stop tasks
tasks = [self.websession.close(), self.api.stop()] self.websession.close()
await asyncio.wait(tasks, loop=self.loop) self.homeassistant.websession.close()
self.exit_code = exit_code # process async stop tasks
self.loop.stop() await asyncio.wait([self.api.stop(), self.dns.stop()], loop=self.loop)
async def _setup_homeassistant(self):
"""Install a homeassistant docker container."""
while True:
# read homeassistant tag and install it
if not self.config.last_homeassistant:
await self.config.fetch_update_infos()
tag = self.config.last_homeassistant
if tag and await self.homeassistant.install(tag):
break
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
await asyncio.sleep(60, loop=self.loop)
# store version
_LOGGER.info("HomeAssistant docker now installed.")
async def _hassio_update(self):
"""Check and run update of supervisor hassio."""
if self.config.last_hassio == self.supervisor.version:
return
_LOGGER.info(
"Found new HassIO version %s.", self.config.last_hassio)
await self.supervisor.update(self.config.last_hassio)

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,266 +1,108 @@
"""Init file for HassIO docker object.""" """Init file for HassIO docker object."""
import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
import docker import docker
from ..tools import get_version_from_env from .network import DockerNetwork
from ..const import SOCKET_DOCKER
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class DockerBase(object): class DockerAPI(object):
"""Docker hassio wrapper.""" """Docker hassio wrapper.
def __init__(self, config, loop, dock, image=None): This class is not AsyncIO safe!
"""
def __init__(self):
"""Initialize docker base wrapper.""" """Initialize docker base wrapper."""
self.config = config self.docker = docker.DockerClient(
self.loop = loop base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
self.dock = dock self.network = DockerNetwork(self.docker)
self.image = image
self.container = None
self.version = None
self._lock = asyncio.Lock(loop=loop)
@property @property
def docker_name(self): def images(self):
"""Return name of docker container.""" """Return api images."""
return None return self.docker.images
@property @property
def in_progress(self): def containers(self):
"""Return True if a task is in progress.""" """Return api containers."""
return self._lock.locked() return self.docker.containers
async def install(self, tag): @property
"""Pull docker image.""" def api(self):
if self._lock.locked(): """Return api containers."""
_LOGGER.error("Can't excute install while a task is in progress") return self.docker.api
return False
async with self._lock: def run(self, image, **kwargs):
return await self.loop.run_in_executor(None, self._install, tag) """"Create a docker and run it.
def _install(self, tag):
"""Pull docker image.
Need run inside executor. Need run inside executor.
""" """
try: name = kwargs.get('name', image)
_LOGGER.info("Pull image %s tag %s.", self.image, tag) network_mode = kwargs.get('network_mode')
image = self.dock.images.pull("{}:{}".format(self.image, tag)) hostname = kwargs.get('hostname')
image.tag(self.image, tag='latest') # setup network
self.version = get_version_from_env(image.attrs['Config']['Env']) if network_mode:
_LOGGER.info("Tag image %s with version %s as latest", kwargs['dns'] = [str(self.network.supervisor)]
self.image, self.version)
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
return False
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:
image = self.dock.images.get(self.image)
self.version = get_version_from_env(image.attrs['Config']['Env'])
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.
"""
if not self.container:
try:
self.container = self.dock.containers.get(self.docker_name)
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
except docker.errors.DockerException:
return False
else: else:
self.container.reload() kwargs['network'] = None
return self.container.status == 'running' # create container
try:
async def attach(self): container = self.docker.containers.create(image, **kwargs)
"""Attach to running docker container.""" except docker.errors.DockerException as err:
if self._lock.locked(): _LOGGER.error("Can't create container from %s -> %s", name, err)
_LOGGER.error("Can't excute attach while a task is in progress")
return False return False
async with self._lock: # attach network
return await self.loop.run_in_executor(None, self._attach) 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:
_LOGGER.warning("Can't attach %s to hassio-net!", name)
def _attach(self): # run container
"""Attach to running docker container.
Need run inside executor.
"""
try: try:
self.container = self.dock.containers.get(self.docker_name) container.start()
self.image = self.container.attrs['Config']['Image'] except docker.errors.DockerException as err:
self.version = get_version_from_env( _LOGGER.error("Can't start %s -> %s", name, err)
self.container.attrs['Config']['Env'])
_LOGGER.info("Attach to image %s with version %s",
self.image, self.version)
except (docker.errors.DockerException, KeyError):
_LOGGER.fatal(
"Can't attach to %s docker container!", self.docker_name)
return False return False
return True return True
async def run(self): def run_command(self, image, command=None, **kwargs):
"""Run docker image.""" """Create a temporary container and run command.
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.
Need run inside executor. Need run inside executor.
""" """
raise NotImplementedError() stdout = kwargs.get('stdout', True)
stderr = kwargs.get('stderr', True)
async def stop(self): _LOGGER.info("Run command '%s' on %s", command, image)
"""Stop/remove docker container.""" try:
if self._lock.locked(): container = self.docker.containers.run(
_LOGGER.error("Can't excute stop while a task is in progress") image,
return False command=command,
network=self.network.name,
**kwargs
)
async with self._lock: # wait until command is done
await self.loop.run_in_executor(None, self._stop) exit_code = container.wait()
return True output = container.logs(stdout=stdout, stderr=stderr)
def _stop(self): except docker.errors.DockerException as err:
"""Stop/remove and remove docker container. _LOGGER.error("Can't execute command -> %s", err)
return (None, b"")
Need run inside executor.
"""
if not self.container:
return
_LOGGER.info("Stop %s docker application", self.image)
self.container.reload()
if self.container.status == 'running':
with suppress(docker.errors.DockerException):
self.container.stop()
# cleanup container
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):
self.container.remove(force=True) container.remove(force=True)
self.container = None return (exit_code, output)
async def remove(self):
"""Remove docker container."""
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 container.
Need run inside executor.
"""
if self._is_running():
self._stop()
_LOGGER.info("Remove docker %s with latest and %s",
self.image, self.version)
try:
self.dock.images.remove(
image="{}:latest".format(self.image), force=True)
self.dock.images.remove(
image="{}:{}".format(self.image, self.version), force=True)
except docker.errors.ImageNotFound:
return True
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False
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.
"""
old_image = "{}:{}".format(self.image, self.version)
_LOGGER.info("Update docker %s with %s:%s",
old_image, self.image, tag)
# update docker image
if self._install(tag):
_LOGGER.info("Cleanup old %s docker", old_image)
self._stop()
try:
self.dock.images.remove(image=old_image, force=True)
except docker.errors.DockerException as err:
_LOGGER.warning(
"Can't remove old image %s -> %s", old_image, err)
return True
return False
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 False
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.
"""
if not self.container:
return
try:
return self.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)

View File

@@ -1,63 +1,175 @@
"""Init file for HassIO addon docker object.""" """Init file for HassIO addon docker object."""
import logging import logging
import os
import docker import docker
import requests
from . import DockerBase from .interface import DockerInterface
from ..tools import get_version_from_env from .util import docker_process
from ..addons.build import AddonBuild
from ..const import (
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant' AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
class DockerAddon(DockerBase): class DockerAddon(DockerInterface):
"""Docker hassio wrapper for HomeAssistant.""" """Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, addons_data, addon): def __init__(self, config, loop, api, addon):
"""Initialize docker homeassistant wrapper.""" """Initialize docker homeassistant wrapper."""
super().__init__( super().__init__(
config, loop, dock, image=addons_data.get_image(addon)) config, loop, api, image=addon.image, timeout=addon.timeout)
self.addon = addon self.addon = addon
self.addons_data = addons_data
def process_metadata(self, metadata, force=False):
"""Use addon data instead meta data with legacy."""
if not self.addon.legacy:
return super().process_metadata(metadata, force=force)
# set meta data
if not self.version or force:
if force: # called on install/update/build
self.version = self.addon.last_version
else:
self.version = self.addon.version_installed
if not self.arch:
self.arch = self.config.arch
@property @property
def docker_name(self): def name(self):
"""Return name of docker container.""" """Return name of docker container."""
return "addon_{}".format(self.addon) 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,
})
# Set api token if any API access is needed
if self.addon.access_hassio_api or self.addon.access_homeassistant_api:
addon_env['API_TOKEN'] = self.addon.api_token
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."""
options = self.addon.tmpfs
if options:
return {"/tmpfs": "{}".format(options)}
return None
@property
def network_mapping(self):
"""Return hosts mapping."""
return {
'homeassistant': self.docker.network.gateway,
'hassio': self.docker.network.supervisor,
}
@property
def network_mode(self):
"""Return network mode for addon."""
if self.addon.host_network:
return 'host'
return None
@property @property
def volumes(self): def volumes(self):
"""Generate volumes for mappings.""" """Generate volumes for mappings."""
volumes = { volumes = {
self.addons_data.path_extern_data(self.addon): { str(self.addon.path_extern_data): {
'bind': '/data', 'mode': 'rw' 'bind': '/data', 'mode': 'rw'
}} }}
if self.addons_data.map_config(self.addon): addon_mapping = self.addon.map_volumes
# setup config mappings
if MAP_CONFIG in addon_mapping:
volumes.update({ volumes.update({
self.config.path_extern_config: { str(self.config.path_extern_config): {
'bind': '/config', 'mode': 'rw' 'bind': '/config', 'mode': addon_mapping[MAP_CONFIG]
}}) }})
if self.addons_data.map_ssl(self.addon): if MAP_SSL in addon_mapping:
volumes.update({ volumes.update({
self.config.path_extern_ssl: { str(self.config.path_extern_ssl): {
'bind': '/ssl', 'mode': 'rw' 'bind': '/ssl', 'mode': addon_mapping[MAP_SSL]
}}) }})
if self.addons_data.map_addons(self.addon): if MAP_ADDONS in addon_mapping:
volumes.update({ volumes.update({
self.config.path_extern_addons_local: { str(self.config.path_extern_addons_local): {
'bind': '/addons', 'mode': 'rw' 'bind': '/addons', 'mode': addon_mapping[MAP_ADDONS]
}}) }})
if self.addons_data.map_backup(self.addon): if MAP_BACKUP in addon_mapping:
volumes.update({ volumes.update({
self.config.path_extern_backup: { str(self.config.path_extern_backup): {
'bind': '/backup', 'mode': 'rw' 'bind': '/backup', 'mode': addon_mapping[MAP_BACKUP]
}}) }})
if MAP_SHARE in addon_mapping:
volumes.update({
str(self.config.path_extern_share): {
'bind': '/share', 'mode': addon_mapping[MAP_SHARE]
}})
# init other hardware mappings
if self.addon.with_gpio:
volumes.update({
'/sys/class/gpio': {
'bind': '/sys/class/gpio', 'mode': "rw"
},
'/sys/devices/platform/soc': {
'bind': '/sys/devices/platform/soc', 'mode': "rw"
},
})
return volumes return volumes
def _run(self): def _run(self):
@@ -66,43 +178,158 @@ class DockerAddon(DockerBase):
Need run inside executor. Need run inside executor.
""" """
if self._is_running(): if self._is_running():
return return True
# cleanup old container # cleanup
self._stop() self._stop()
try: # write config
self.container = self.dock.containers.run( if not self.addon.write_options():
self.image, return False
name=self.docker_name,
detach=True,
network_mode='bridge',
ports=self.addons_data.get_ports(self.addon),
volumes=self.volumes,
)
self.version = get_version_from_env( ret = self.docker.run(
self.container.attrs['Config']['Env']) self.image,
name=self.name,
hostname=self.hostname,
detach=True,
init=True,
stdin_open=self.addon.with_stdin,
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
)
if ret:
_LOGGER.info("Start docker addon %s with version %s", _LOGGER.info("Start docker addon %s with version %s",
self.image, self.version) self.image, self.version)
except docker.errors.DockerException as err: return ret
_LOGGER.error("Can't run %s -> %s", self.image, err)
def _install(self, tag):
"""Pull docker image or build it.
Need run inside executor.
"""
if self.addon.need_build:
return self._build(tag)
return super()._install(tag)
def _build(self, tag):
"""Build a docker container.
Need run inside executor.
"""
build_env = AddonBuild(self.config, self.addon)
_LOGGER.info("Start build %s:%s", self.image, tag)
try:
image = self.docker.images.build(**build_env.get_docker_args(tag))
image.tag(self.image, tag='latest')
self.process_metadata(image.attrs, force=True)
except (docker.errors.DockerException) as err:
_LOGGER.error("Can't build %s:%s -> %s", self.image, tag, err)
return False return False
_LOGGER.info("Build %s:%s done", self.image, tag)
return True return True
def _attach(self): @docker_process
"""Attach to running docker container. def export_image(self, path):
"""Export current images into a tar file."""
return self.loop.run_in_executor(None, self._export_image, path)
def _export_image(self, tar_file):
"""Export current images into a tar file.
Need run inside executor. Need run inside executor.
""" """
try: try:
self.container = self.dock.containers.get(self.docker_name) image = self.docker.api.get_image(self.image)
self.version = get_version_from_env( except docker.errors.DockerException as err:
self.container.attrs['Config']['Env']) _LOGGER.error("Can't fetch image %s -> %s", self.image, err)
_LOGGER.info("Attach to image %s with version %s", return False
self.image, self.version)
except (docker.errors.DockerException, KeyError): try:
pass with tar_file.open("wb") as write_tar:
for chunk in image.stream():
write_tar.write(chunk)
except (OSError, requests.exceptions.ReadTimeout) as err:
_LOGGER.error("Can't write tar file %s -> %s", tar_file, err)
return False
_LOGGER.info("Export image %s to %s", self.image, tar_file)
return True
@docker_process
def import_image(self, path, tag):
"""Import a tar file as image."""
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.
Need run inside executor.
"""
try:
with tar_file.open("rb") as read_tar:
self.docker.api.load_image(read_tar)
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)
return False
_LOGGER.info("Import image %s and tag %s", tar_file, tag)
self.process_metadata(image.attrs, force=True)
self._cleanup()
return True
def _restart(self):
"""Restart docker container.
Addons prepare some thing on start and that is normaly not repeatable.
Need run inside executor.
"""
self._stop()
return self._run()
@docker_process
def write_stdin(self, data):
"""Write to add-on stdin."""
return self.loop.run_in_executor(None, self._write_stdin, data)
def _write_stdin(self, data):
"""Write to add-on stdin.
Need run inside executor.
"""
if not self._is_running():
return False
try:
# load needed docker objects
container = self.docker.containers.get(self.name)
socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
except docker.errors.DockerException as err:
_LOGGER.error("Can't attach to %s stdin -> %s", self.name, err)
return False
try:
# write to stdin
data += b"\n"
os.write(socket.fileno(), data)
socket.close()
except OSError as err:
_LOGGER.error("Can't write to %s stdin -> %s", self.name, err)
return False
return True

View File

@@ -3,26 +3,38 @@ import logging
import docker import docker
from . import DockerBase from .interface import DockerInterface
from ..tools import get_version_from_env
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant' HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerBase): class DockerHomeAssistant(DockerInterface):
"""Docker hassio wrapper for HomeAssistant.""" """Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock): def __init__(self, config, loop, api, data):
"""Initialize docker homeassistant wrapper.""" """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 @property
def docker_name(self): def name(self):
"""Return name of docker container.""" """Return name of docker container."""
return HASS_DOCKER_NAME return HASS_DOCKER_NAME
@property
def devices(self):
"""Create list of special device to map into docker."""
if not self.data.devices:
return
devices = []
for device in self.data.devices:
devices.append("/dev/{0}:/dev/{0}:rwm".format(device))
return devices
def _run(self): def _run(self):
"""Run docker image. """Run docker image.
@@ -31,47 +43,72 @@ class DockerHomeAssistant(DockerBase):
if self._is_running(): if self._is_running():
return return
# cleanup old container # cleanup
self._stop() self._stop()
try: ret = self.docker.run(
self.container = self.dock.containers.run( self.image,
self.image, name=self.name,
name=self.docker_name, hostname=self.name,
detach=True, detach=True,
privileged=True, privileged=True,
network_mode='host', init=True,
environment={ devices=self.devices,
'HASSIO': self.config.api_endpoint, network_mode='host',
}, environment={
volumes={ 'HASSIO': self.docker.network.supervisor,
self.config.path_extern_config: 'TZ': self.config.timezone,
{'bind': '/config', 'mode': 'rw'}, },
self.config.path_extern_ssl: volumes={
{'bind': '/ssl', 'mode': 'rw'}, 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'},
}
)
self.version = get_version_from_env( if ret:
self.container.attrs['Config']['Env']) _LOGGER.info("Start homeassistant %s with version %s",
_LOGGER.info("Start docker addon %s with version %s",
self.image, self.version) self.image, self.version)
except docker.errors.DockerException as err: return ret
_LOGGER.error("Can't run %s -> %s", self.image, err)
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 return False
return True return True
async def update(self, tag):
"""Update homeassistant docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
async with self._lock:
if await self.loop.run_in_executor(None, self._update, tag):
await self.loop.run_in_executor(None, self._run)
return True
return False

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

@@ -4,67 +4,57 @@ import os
import docker import docker
from . import DockerBase from .interface import DockerInterface
from ..const import RESTART_EXIT_CODE from .util import docker_process
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerBase): class DockerSupervisor(DockerInterface):
"""Docker hassio wrapper for HomeAssistant.""" """Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, hassio, image=None): def __init__(self, config, loop, api, stop_callback, image=None):
"""Initialize docker base wrapper.""" """Initialize docker base wrapper."""
super().__init__(config, loop, dock, image=image) super().__init__(config, loop, api, image=image)
self.stop_callback = stop_callback
self.hassio = hassio
@property @property
def docker_name(self): def name(self):
"""Return name of docker container.""" """Return name of docker container."""
return os.environ['SUPERVISOR_NAME'] return os.environ['SUPERVISOR_NAME']
async def update(self, tag): def _attach(self):
"""Update a supervisor docker image.""" """Attach to running docker container.
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)
old_version = self.version
async with self._lock:
if await self.loop.run_in_executor(None, self._install, tag):
self.config.hassio_cleanup = old_version
self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE))
return True
return False
async def cleanup(self):
"""Check if old supervisor version exists and cleanup."""
if not self.config.hassio_cleanup:
return
async with self._lock:
if await self.loop.run_in_executor(None, self._cleanup):
self.config.hassio_cleanup = None
def _cleanup(self):
"""Remove old image.
Need run inside executor. Need run inside executor.
""" """
old_image = "{}:{}".format(self.image, self.config.hassio_cleanup)
_LOGGER.info("Old supervisor docker found %s", old_image)
try: try:
self.dock.images.remove(image=old_image, force=True) container = self.docker.containers.get(self.name)
except docker.errors.DockerException as err: except docker.errors.DockerException:
_LOGGER.warning("Can't remove old image %s -> %s", old_image, err)
return False return False
return True 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."""
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.call_later(1, self.loop.stop)
return True
return False
async def run(self): async def run(self):
"""Run docker image.""" """Run docker image."""
@@ -81,3 +71,7 @@ class DockerSupervisor(DockerBase):
async def remove(self): async def remove(self):
"""Remove docker image.""" """Remove docker image."""
raise RuntimeError("Not support on supervisor docker container!") raise RuntimeError("Not support on supervisor docker container!")
async def restart(self):
"""Restart docker container."""
raise RuntimeError("Not support on supervisor docker container!")

20
hassio/dock/util.py Normal file
View File

@@ -0,0 +1,20 @@
"""HassIO docker utilitys."""
import logging
_LOGGER = logging.getLogger(__name__)
# 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

120
hassio/hardware.py Normal file
View File

@@ -0,0 +1,120 @@
"""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+)")
GPIO_DEVICES = Path("/sys/class/gpio")
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 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 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 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 gpio_devices(self):
"""Return list of GPIO interface on device."""
dev_list = set()
for interface in GPIO_DEVICES.glob("gpio*"):
dev_list.add(interface.name)
return dev_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)))

290
hassio/homeassistant.py Normal file
View File

@@ -0,0 +1,290 @@
"""HomeAssistant control object."""
import asyncio
import logging
import os
import re
import aiohttp
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
HEADER_HA_ACCESS, CONTENT_TYPE_JSON)
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)
self.api_ip = docker.network.gateway
self.websession = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False), loop=loop)
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 api_port(self):
"""Return network port to home-assistant instance."""
return self._data[ATTR_PORT]
@api_port.setter
def api_port(self, value):
"""Set network port for home-assistant instance."""
self._data[ATTR_PORT] = value
self.save()
@property
def api_password(self):
"""Return password for home-assistant instance."""
return self._data.get(ATTR_PASSWORD)
@api_password.setter
def api_password(self, value):
"""Set password for home-assistant instance."""
self._data[ATTR_PASSWORD] = value
self.save()
@property
def api_ssl(self):
"""Return if we need ssl to home-assistant instance."""
return self._data[ATTR_SSL]
@api_ssl.setter
def api_ssl(self, value):
"""Set SSL for home-assistant instance."""
self._data[ATTR_SSL] = value
self.save()
@property
def api_url(self):
"""Return API url to Home-Assistant."""
return "{}://{}:{}".format(
'https' if self.api_ssl else 'http', self.api_ip, self.api_port
)
@property
def watchdog(self):
"""Return True if the watchdog should protect Home-Assistant."""
return self._data[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value):
"""Return True if the watchdog should protect Home-Assistant."""
self._data[ATTR_WATCHDOG] = value
self.save()
@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)
# run landingpage after installation
await self.docker.run()
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)
# finishing
_LOGGER.info("HomeAssistant docker now installed")
if self.boot:
await self.docker.run()
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)
async def check_api_state(self):
"""Check if Home-Assistant up and running."""
url = "{}/api/".format(self.api_url)
header = {CONTENT_TYPE: CONTENT_TYPE_JSON}
if self.api_password:
header.update({HEADER_HA_ACCESS: self.api_password})
try:
async with async_timeout.timeout(30, loop=self.loop):
async with self.websession.get(url, headers=header) as request:
status = request.status
except (asyncio.TimeoutError, aiohttp.ClientError):
return False
if status not in (200, 201):
_LOGGER.warning("Home-Assistant API config missmatch")
return True

View File

@@ -17,6 +17,7 @@ UNKNOWN = 'unknown'
FEATURES_SHUTDOWN = 'shutdown' FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot' FEATURES_REBOOT = 'reboot'
FEATURES_UPDATE = 'update' FEATURES_UPDATE = 'update'
FEATURES_HOSTNAME = 'hostname'
FEATURES_NETWORK_INFO = 'network_info' FEATURES_NETWORK_INFO = 'network_info'
FEATURES_NETWORK_CONTROL = 'network_control' FEATURES_NETWORK_CONTROL = 'network_control'
@@ -117,3 +118,7 @@ class HostControl(object):
if version: if version:
return self._send_command("update {}".format(version)) return self._send_command("update {}".format(version))
return self._send_command("update") return self._send_command("update")
def set_hostname(self, hostname):
"""Update hostname on host."""
return self._send_command("hostname {}".format(hostname))

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -16,11 +16,7 @@ class Scheduler(object):
"""Initialize task schedule.""" """Initialize task schedule."""
self.loop = loop self.loop = loop
self._data = {} self._data = {}
self._stop = False self.suspend = False
def stop(self):
"""Stop to execute tasks in scheduler."""
self._stop = True
def register_task(self, coro_callback, seconds, repeat=True, def register_task(self, coro_callback, seconds, repeat=True,
now=False): now=False):
@@ -51,11 +47,8 @@ class Scheduler(object):
"""Run a scheduled task.""" """Run a scheduled task."""
data = self._data.pop(idx) data = self._data.pop(idx)
# stop execute tasks if not self.suspend:
if self._stop: self.loop.create_task(data[CALL]())
return
self.loop.create_task(data[CALL]())
if data[REPEAT]: if data[REPEAT]:
task = self.loop.call_later(data[SEC], self._run_task, idx) task = self.loop.call_later(data[SEC], self._run_task, idx)

View File

@@ -0,0 +1,314 @@
"""Snapshot system control."""
import asyncio
from datetime import datetime
import logging
from pathlib import Path
import tarfile
from .snapshot import Snapshot
from .util import create_slug
from ..const import (
ATTR_SLUG, FOLDER_HOMEASSISTANT, SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
_LOGGER = logging.getLogger(__name__)
class SnapshotsManager(object):
"""Manage snapshots."""
def __init__(self, config, loop, sheduler, addons, homeassistant):
"""Initialize a snapshot manager."""
self.config = config
self.loop = loop
self.sheduler = sheduler
self.addons = addons
self.homeassistant = homeassistant
self.snapshots = {}
self._lock = asyncio.Lock(loop=loop)
@property
def list_snapshots(self):
"""Return a list of all snapshot object."""
return set(self.snapshots.values())
def get(self, slug):
"""Return snapshot object."""
return self.snapshots.get(slug)
def _create_snapshot(self, name, sys_type):
"""Initialize a new snapshot object from name."""
date_str = datetime.utcnow().isoformat()
slug = create_slug(name, date_str)
tar_file = Path(self.config.path_backup, "{}.tar".format(slug))
# init object
snapshot = Snapshot(self.config, self.loop, tar_file)
snapshot.create(slug, name, date_str, sys_type)
# set general data
snapshot.snapshot_homeassistant(self.homeassistant)
snapshot.repositories = self.config.addons_repositories
return snapshot
async def reload(self):
"""Load exists backups."""
self.snapshots = {}
async def _load_snapshot(tar_file):
"""Internal function to load snapshot."""
snapshot = Snapshot(self.config, self.loop, tar_file)
if await snapshot.load():
self.snapshots[snapshot.slug] = snapshot
tasks = [_load_snapshot(tar_file) for tar_file in
self.config.path_backup.glob("*.tar")]
_LOGGER.info("Found %d snapshot files", len(tasks))
if tasks:
await asyncio.wait(tasks, loop=self.loop)
def remove(self, snapshot):
"""Remove a snapshot."""
try:
snapshot.tar_file.unlink()
self.snapshots.pop(snapshot.slug, None)
except OSError as err:
_LOGGER.error("Can't remove snapshot %s -> %s", snapshot.slug, err)
return False
return True
async def do_snapshot_full(self, name=""):
"""Create a full snapshot."""
if self._lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
snapshot = self._create_snapshot(name, SNAPSHOT_FULL)
_LOGGER.info("Full-Snapshot %s start", snapshot.slug)
try:
self.sheduler.suspend = True
await self._lock.acquire()
async with snapshot:
# snapshot addons
tasks = []
for addon in self.addons.list_addons:
if not addon.is_installed:
continue
tasks.append(snapshot.import_addon(addon))
if tasks:
_LOGGER.info("Full-Snapshot %s run %d addons",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self.loop)
# snapshot folders
_LOGGER.info("Full-Snapshot %s store folders", snapshot.slug)
await snapshot.store_folders()
_LOGGER.info("Full-Snapshot %s done", snapshot.slug)
self.snapshots[snapshot.slug] = snapshot
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Snapshot %s error -> %s", snapshot.slug, err)
return False
finally:
self.sheduler.suspend = False
self._lock.release()
async def do_snapshot_partial(self, name="", addons=None, folders=None):
"""Create a partial snapshot."""
if self._lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
addons = addons or []
folders = folders or []
snapshot = self._create_snapshot(name, SNAPSHOT_PARTIAL)
_LOGGER.info("Partial-Snapshot %s start", snapshot.slug)
try:
self.sheduler.suspend = True
await self._lock.acquire()
async with snapshot:
# snapshot addons
tasks = []
for slug in addons:
addon = self.addons.get(slug)
if addon.is_installed:
tasks.append(snapshot.import_addon(addon))
if tasks:
_LOGGER.info("Partial-Snapshot %s run %d addons",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self.loop)
# snapshot folders
_LOGGER.info("Partial-Snapshot %s store folders %s",
snapshot.slug, folders)
await snapshot.store_folders(folders)
_LOGGER.info("Partial-Snapshot %s done", snapshot.slug)
self.snapshots[snapshot.slug] = snapshot
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Snapshot %s error -> %s", snapshot.slug, err)
return False
finally:
self.sheduler.suspend = False
self._lock.release()
async def do_restore_full(self, snapshot):
"""Restore a snapshot."""
if self._lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
if snapshot.sys_type != SNAPSHOT_FULL:
_LOGGER.error(
"Full-Restore %s is only a partial snapshot!", snapshot.slug)
return False
_LOGGER.info("Full-Restore %s start", snapshot.slug)
try:
self.sheduler.suspend = True
await self._lock.acquire()
async with snapshot:
# stop system
tasks = []
tasks.append(self.homeassistant.stop())
for addon in self.addons.list_addons:
if addon.is_installed:
tasks.append(addon.stop())
await asyncio.wait(tasks, loop=self.loop)
# restore folders
_LOGGER.info("Full-Restore %s restore folders", snapshot.slug)
await snapshot.restore_folders()
# start homeassistant restore
_LOGGER.info("Full-Restore %s restore Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant(self.homeassistant)
task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant_version))
# restore repositories
await self.addons.load_repositories(snapshot.repositories)
# restore addons
tasks = []
actual_addons = \
set(addon.slug for addon in self.addons.list_addons
if addon.is_installed)
restore_addons = \
set(data[ATTR_SLUG] for data in snapshot.addons)
remove_addons = actual_addons - restore_addons
_LOGGER.info("Full-Restore %s restore addons %s, remove %s",
snapshot.slug, restore_addons, remove_addons)
for slug in remove_addons:
addon = self.addons.get(slug)
if addon:
tasks.append(addon.uninstall())
else:
_LOGGER.warning("Can't remove addon %s", slug)
for slug in restore_addons:
addon = self.addons.get(slug)
if addon:
tasks.append(snapshot.export_addon(addon))
else:
_LOGGER.warning("Can't restore addon %s", slug)
if tasks:
_LOGGER.info("Full-Restore %s restore addons tasks %d",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self.loop)
# finish homeassistant task
_LOGGER.info("Full-Restore %s wait until homeassistant ready",
snapshot.slug)
await task_hass
await self.homeassistant.run()
_LOGGER.info("Full-Restore %s done", snapshot.slug)
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Restore %s error -> %s", slug, err)
return False
finally:
self.sheduler.suspend = False
self._lock.release()
async def do_restore_partial(self, snapshot, homeassistant=False,
addons=None, folders=None):
"""Restore a snapshot."""
if self._lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
addons = addons or []
folders = folders or []
_LOGGER.info("Partial-Restore %s start", snapshot.slug)
try:
self.sheduler.suspend = True
await self._lock.acquire()
async with snapshot:
tasks = []
if FOLDER_HOMEASSISTANT in folders:
await self.homeassistant.stop()
if folders:
_LOGGER.info("Partial-Restore %s restore folders %s",
snapshot.slug, folders)
await snapshot.restore_folders(folders)
if homeassistant:
_LOGGER.info("Partial-Restore %s restore Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant(self.homeassistant)
tasks.append(self.homeassistant.update(
snapshot.homeassistant_version))
for slug in addons:
addon = self.addons.get(slug)
if addon:
tasks.append(snapshot.export_addon(addon))
else:
_LOGGER.warning("Can't restore addon %s", slug)
if tasks:
_LOGGER.info("Partial-Restore %s run %d tasks",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self.loop)
# make sure homeassistant run agen
await self.homeassistant.run()
_LOGGER.info("Partial-Restore %s done", snapshot.slug)
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Restore %s error -> %s", slug, err)
return False
finally:
self.sheduler.suspend = False
self._lock.release()

View File

@@ -0,0 +1,368 @@
"""Represent a snapshot file."""
import asyncio
import json
import logging
from pathlib import Path
import tarfile
from tempfile import TemporaryDirectory
import voluptuous as vol
from voluptuous.humanize import humanize_error
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_IMAGE, ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT)
from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__)
class Snapshot(object):
"""A signle hassio snapshot."""
def __init__(self, config, loop, tar_file):
"""Initialize a snapshot."""
self.loop = loop
self.config = config
self.tar_file = tar_file
self._data = {}
self._tmp = None
@property
def slug(self):
"""Return snapshot slug."""
return self._data.get(ATTR_SLUG)
@property
def sys_type(self):
"""Return snapshot type."""
return self._data.get(ATTR_TYPE)
@property
def name(self):
"""Return snapshot name."""
return self._data[ATTR_NAME]
@property
def date(self):
"""Return snapshot date."""
return self._data[ATTR_DATE]
@property
def addons(self):
"""Return snapshot date."""
return self._data[ATTR_ADDONS]
@property
def folders(self):
"""Return list of saved folders."""
return self._data[ATTR_FOLDERS]
@property
def repositories(self):
"""Return snapshot date."""
return self._data[ATTR_REPOSITORIES]
@repositories.setter
def repositories(self, value):
"""Set snapshot date."""
self._data[ATTR_REPOSITORIES] = value
@property
def homeassistant_version(self):
"""Return snapshot homeassistant version."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION)
@homeassistant_version.setter
def homeassistant_version(self, value):
"""Set snapshot homeassistant version."""
self._data[ATTR_HOMEASSISTANT][ATTR_VERSION] = value
@property
def homeassistant_devices(self):
"""Return snapshot homeassistant devices."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_DEVICES)
@homeassistant_devices.setter
def homeassistant_devices(self, value):
"""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 homeassistant_ssl(self):
"""Return snapshot homeassistant api ssl."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_SSL)
@homeassistant_ssl.setter
def homeassistant_ssl(self, value):
"""Set snapshot homeassistant api ssl."""
self._data[ATTR_HOMEASSISTANT][ATTR_SSL] = value
@property
def homeassistant_port(self):
"""Return snapshot homeassistant api port."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PORT)
@homeassistant_port.setter
def homeassistant_port(self, value):
"""Set snapshot homeassistant api port."""
self._data[ATTR_HOMEASSISTANT][ATTR_PORT] = value
@property
def homeassistant_password(self):
"""Return snapshot homeassistant api password."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PASSWORD)
@homeassistant_password.setter
def homeassistant_password(self, value):
"""Set snapshot homeassistant api password."""
self._data[ATTR_HOMEASSISTANT][ATTR_PASSWORD] = value
@property
def homeassistant_watchdog(self):
"""Return snapshot homeassistant watchdog options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_WATCHDOG)
@homeassistant_watchdog.setter
def homeassistant_watchdog(self, value):
"""Set snapshot homeassistant watchdog options."""
self._data[ATTR_HOMEASSISTANT][ATTR_WATCHDOG] = value
@property
def homeassistant_boot(self):
"""Return snapshot homeassistant boot options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_BOOT)
@homeassistant_boot.setter
def homeassistant_boot(self, value):
"""Set snapshot homeassistant boot options."""
self._data[ATTR_HOMEASSISTANT][ATTR_BOOT] = value
@property
def size(self):
"""Return snapshot size."""
if not self.tar_file.is_file():
return 0
return self.tar_file.stat().st_size / 1048576 # calc mbyte
def create(self, slug, name, date, sys_type):
"""Initialize a new snapshot."""
# init metadata
self._data[ATTR_SLUG] = slug
self._data[ATTR_NAME] = name
self._data[ATTR_DATE] = date
self._data[ATTR_TYPE] = sys_type
# init other constructs
self._data[ATTR_HOMEASSISTANT] = {}
self._data[ATTR_ADDONS] = []
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
self.homeassistant_watchdog = homeassistant.watchdog
self.homeassistant_boot = homeassistant.boot
# custom image
if homeassistant.is_custom_image:
self.homeassistant_image = homeassistant.image
# api
self.homeassistant_port = homeassistant.api_port
self.homeassistant_ssl = homeassistant.api_ssl
self.homeassistant_password = homeassistant.api_password
def restore_homeassistant(self, homeassistant):
"""Write all data to homeassistant object."""
homeassistant.devices = self.homeassistant_devices
homeassistant.watchdog = self.homeassistant_watchdog
homeassistant.boot = self.homeassistant_boot
# custom image
if self.homeassistant_image:
homeassistant.set_custom(
self.homeassistant_image, self.homeassistant_version)
# api
homeassistant.api_port = self.homeassistant_port
homeassistant.api_ssl = self.homeassistant_ssl
homeassistant.api_password = self.homeassistant_password
async def load(self):
"""Read snapshot.json from tar file."""
if not self.tar_file.is_file():
_LOGGER.error("No tarfile %s", self.tar_file)
return False
def _load_file():
"""Read snapshot.json."""
with tarfile.open(self.tar_file, "r:") as snapshot:
json_file = snapshot.extractfile("./snapshot.json")
return json_file.read()
# read snapshot.json
try:
raw = await self.loop.run_in_executor(None, _load_file)
except (tarfile.TarError, KeyError) as err:
_LOGGER.error(
"Can't read snapshot tarfile %s -> %s", self.tar_file, err)
return False
# parse data
try:
raw_dict = json.loads(raw)
except json.JSONDecodeError as err:
_LOGGER.error("Can't read data for %s -> %s", self.tar_file, err)
return False
# validate
try:
self._data = SCHEMA_SNAPSHOT(raw_dict)
except vol.Invalid as err:
_LOGGER.error("Can't validate data for %s -> %s", self.tar_file,
humanize_error(raw_dict, err))
return False
return True
async def __aenter__(self):
"""Async context to open a snapshot."""
self._tmp = TemporaryDirectory(dir=str(self.config.path_tmp))
# create a snapshot
if not self.tar_file.is_file():
return self
# extract a exists snapshot
def _extract_snapshot():
"""Extract a snapshot."""
with tarfile.open(self.tar_file, "r:") as tar:
tar.extractall(path=self._tmp.name)
await self.loop.run_in_executor(None, _extract_snapshot)
async def __aexit__(self, exception_type, exception_value, traceback):
"""Async context to close a snapshot."""
# exists snapshot or exception on build
if self.tar_file.is_file() or exception_type is not None:
self._tmp.cleanup()
return
# validate data
try:
self._data = SCHEMA_SNAPSHOT(self._data)
except vol.Invalid as err:
_LOGGER.error("Invalid data for %s -> %s", self.tar_file,
humanize_error(self._data, err))
raise ValueError("Invalid config") from None
# new snapshot, build it
def _create_snapshot():
"""Create a new snapshot."""
with tarfile.open(self.tar_file, "w:") as tar:
tar.add(self._tmp.name, arcname=".")
if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data):
await self.loop.run_in_executor(None, _create_snapshot)
else:
_LOGGER.error("Can't write snapshot.json")
self._tmp.cleanup()
async def import_addon(self, addon):
"""Add a addon into snapshot."""
snapshot_file = Path(self._tmp.name, "{}.tar.gz".format(addon.slug))
if not await addon.snapshot(snapshot_file):
_LOGGER.error("Can't make snapshot from %s", addon.slug)
return False
# store to config
self._data[ATTR_ADDONS].append({
ATTR_SLUG: addon.slug,
ATTR_NAME: addon.name,
ATTR_VERSION: addon.version_installed,
})
return True
async def export_addon(self, addon):
"""Restore a addon from snapshot."""
snapshot_file = Path(self._tmp.name, "{}.tar.gz".format(addon.slug))
if not await addon.restore(snapshot_file):
_LOGGER.error("Can't restore snapshot for %s", addon.slug)
return False
return True
async def store_folders(self, folder_list=None):
"""Backup hassio data into snapshot."""
folder_list = folder_list or ALL_FOLDERS
def _folder_save(name):
"""Intenal function to snapshot a folder."""
slug_name = name.replace("/", "_")
snapshot_tar = Path(self._tmp.name, "{}.tar.gz".format(slug_name))
origin_dir = Path(self.config.path_hassio, name)
try:
_LOGGER.info("Snapshot folder %s", name)
with tarfile.open(snapshot_tar, "w:gz",
compresslevel=1) as tar_file:
tar_file.add(origin_dir, arcname=".")
_LOGGER.info("Snapshot folder %s done", name)
self._data[ATTR_FOLDERS].append(name)
except tarfile.TarError as err:
_LOGGER.warning("Can't snapshot folder %s -> %s", name, err)
# run tasks
tasks = [self.loop.run_in_executor(None, _folder_save, folder)
for folder in folder_list]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
async def restore_folders(self, folder_list=None):
"""Backup hassio data into snapshot."""
folder_list = folder_list or ALL_FOLDERS
def _folder_restore(name):
"""Intenal function to restore a folder."""
slug_name = name.replace("/", "_")
snapshot_tar = Path(self._tmp.name, "{}.tar.gz".format(slug_name))
origin_dir = Path(self.config.path_hassio, name)
# clean old stuff
if origin_dir.is_dir():
remove_folder(origin_dir)
try:
_LOGGER.info("Restore folder %s", name)
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
tar_file.extractall(path=origin_dir)
_LOGGER.info("Restore folder %s done", name)
except tarfile.TarError as err:
_LOGGER.warning("Can't restore folder %s -> %s", name, err)
# run tasks
tasks = [self.loop.run_in_executor(None, _folder_restore, folder)
for folder in folder_list]
if tasks:
await asyncio.wait(tasks, loop=self.loop)

21
hassio/snapshots/util.py Normal file
View File

@@ -0,0 +1,21 @@
"""Util addons functions."""
import hashlib
import shutil
def create_slug(name, date_str):
"""Generate a hash from repository."""
key = "{} - {}".format(date_str, name).lower().encode()
return hashlib.sha1(key).hexdigest()[:8]
def remove_folder(folder):
"""Remove folder data but not the folder itself."""
for obj in folder.iterdir():
try:
if obj.is_dir():
shutil.rmtree(str(obj), ignore_errors=True)
else:
obj.unlink()
except (OSError, shutil.Error):
pass

View File

@@ -0,0 +1,38 @@
"""Validate some things around restore."""
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,
ATTR_IMAGE, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES, NETWORK_PORT
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
# pylint: disable=no-value-for-parameter
SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_TYPE): vol.In([SNAPSHOT_FULL, SNAPSHOT_PARTIAL]),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_DATE): vol.Coerce(str),
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_BOOT, default=True): vol.Boolean(),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
}),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
})],
vol.Optional(ATTR_REPOSITORIES, default=[]): [vol.Url()],
}, extra=vol.ALLOW_EXTRA)

115
hassio/tasks.py Normal file
View File

@@ -0,0 +1,115 @@
"""Multible tasks."""
import asyncio
from datetime import datetime
import logging
_LOGGER = logging.getLogger(__name__)
def api_sessions_cleanup(config):
"""Create scheduler task for cleanup api sessions."""
async def _api_sessions_cleanup():
"""Cleanup old api sessions."""
now = datetime.now()
for session, until_valid in config.security_sessions.items():
if now >= until_valid:
config.drop_security_session(session)
return _api_sessions_cleanup
def addons_update(loop, addons):
"""Create scheduler task for auto update addons."""
async def _addons_update():
"""Check if a update is available of a addon and update it."""
tasks = []
for addon in addons.list_addons:
if not addon.is_installed or not addon.auto_update:
continue
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))
await asyncio.wait(tasks, loop=loop)
return _addons_update
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 updater.fetch_data()
if updater.version_hassio == supervisor.version:
return
# don't perform a update on beta/dev channel
if updater.beta_channel:
_LOGGER.warning("Ignore Hass.IO update on beta upstream!")
return
_LOGGER.info("Found new HassIO version %s.", updater.version_hassio)
await supervisor.update(updater.version_hassio)
return _hassio_update
def homeassistant_watchdog_docker(loop, homeassistant):
"""Create scheduler task for montoring running state of docker."""
async def _homeassistant_watchdog_docker():
"""Check running state of docker and start if they is close."""
# if Home-Assistant is active
if not await homeassistant.is_initialize() or \
not homeassistant.watchdog:
return
# if Home-Assistant is running
if homeassistant.in_progress or await homeassistant.is_running():
return
loop.create_task(homeassistant.run())
_LOGGER.error("Watchdog found a problem with Home-Assistant docker!")
return _homeassistant_watchdog_docker
def homeassistant_watchdog_api(loop, homeassistant):
"""Create scheduler task for montoring running state of API.
Try 2 times to call API before we restart Home-Assistant. Maybe we had a
delay in our system.
"""
retry_scan = 0
async def _homeassistant_watchdog_api():
"""Check running state of API and start if they is close."""
nonlocal retry_scan
# if Home-Assistant is active
if not await homeassistant.is_initialize() or \
not homeassistant.watchdog:
return
# if Home-Assistant API is up
if homeassistant.in_progress or await homeassistant.check_api_state():
return
retry_scan += 1
# Retry active
if retry_scan == 1:
_LOGGER.warning("Watchdog miss API response from Home-Assistant")
return
loop.create_task(homeassistant.restart())
_LOGGER.error("Watchdog found a problem with Home-Assistant API!")
retry_scan = 0
return _homeassistant_watchdog_api

View File

@@ -1,77 +1,32 @@
"""Tools file for HassIO.""" """Tools file for HassIO."""
import asyncio import asyncio
from contextlib import suppress
from datetime import datetime, timedelta, timezone
import json import json
import logging import logging
import re import re
import socket
import aiohttp import aiohttp
import async_timeout import async_timeout
import pytz
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA import voluptuous as vol
from voluptuous.humanize import humanize_error
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_RE_VERSION = re.compile(r"VERSION=(.*)") FREEGEOIP_URL = "https://freegeoip.io/json/"
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
async def fetch_last_versions(websession, beta=False): # Copyright (c) Django Software Foundation and individual contributors.
"""Fetch current versions from github. # All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
Is a coroutine. DATETIME_RE = re.compile(
""" r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
url = URL_HASSIO_VERSION_BETA if beta else URL_HASSIO_VERSION r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
try: r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
with async_timeout.timeout(10, loop=websession.loop): r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
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_arch_from_image(image):
"""Return arch from hassio image name."""
found = _IMAGE_ARCH.match(image)
if found:
return found.group(1)
def get_version_from_env(env_list):
"""Extract Version from ENV list."""
for env in env_list:
found = _RE_VERSION.match(env)
if found:
return found.group(1)
_LOGGER.error("Can't find VERSION in env")
return None
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)
def write_json_file(jsonfile, data): def write_json_file(jsonfile, data):
@@ -90,3 +45,123 @@ def read_json_file(jsonfile):
"""Read a json file and return a dict.""" """Read a json file and return a dict."""
with jsonfile.open('r') as cfile: with jsonfile.open('r') as cfile:
return json.loads(cfile.read()) return json.loads(cfile.read())
async def fetch_timezone(websession):
"""Read timezone from freegeoip."""
data = {}
with suppress(aiohttp.ClientError, asyncio.TimeoutError,
json.JSONDecodeError, KeyError):
with async_timeout.timeout(10, loop=websession.loop):
async with websession.get(FREEGEOIP_URL) as request:
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))
# reset data to default
self._data = self._schema({})
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()

92
hassio/validate.py Normal file
View File

@@ -0,0 +1,92 @@
"""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, ATTR_SSL,
ATTR_PORT, ATTR_WATCHDOG)
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):
"""Convert data into docker port list."""
# dynamic ports
if data is None:
return
# single port
if isinstance(data, int):
return NETWORK_PORT(data)
# port list
if isinstance(data, list) and len(data) > 2:
return vol.Schema([NETWORK_PORT])(data)
# ip port mapping
if isinstance(data, list) and len(data) == 2:
return (vol.Coerce(str)(data[0]), NETWORK_PORT(data[1]))
raise vol.Invalid("Can't validate docker host settings")
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),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
}, extra=vol.REMOVE_EXTRA)
# 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),
}, extra=vol.REMOVE_EXTRA)
# 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)

BIN
misc/security.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

1
misc/security.xml Normal file
View File

@@ -0,0 +1 @@
<mxfile userAgent="Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0" version="6.5.8" editor="www.draw.io" type="device"><diagram name="Page-1">5Vxdd5s4EP01fmwOkgCbx9hp2j7sNrvpnnYfiVFsTjDyghwn++tXGMmAxileEB9O+9BjBhjM3GHmzjXKhCw2L58Sf7v+jQU0mmAreJmQmwnGU4eI/zPDa24gyM0NqyQMchMqDPfhv1QaLWndhQFNKwdyxiIebqvGJYtjuuQVm58kbF897JFF1atu/RUFhvulH0Hr9zDg69w6w25h/0zD1VpdGblevufBXz6tEraL5fUmmDwe/uW7N77yJW80XfsB25dM5OOELBLGeP5p87KgURZaFbb8vNs39h6/d0Jjfs4JOD/h2Y928tZvwyTlwnTP/YTLL8lfVWA4fRF+52u+iYQBiY8pT9gTXbCIJcISs1gcOX8Mo0gz+VG4isXmUnwzKuzzZ5rwUIT8Wu7YhEGQXWa+X4ec3m/9ZXbNvcivzCGL+b38Go7aztMGeWIb3rcMRXYV+lIyyTh8omxDefIqDpF7ySw/Q6asKxHaF/gjS9rWJewVkr5MudXRcRF28UFG/jQKBKDwVypipAe/FPUtC2N+uKIznzg3mYUmobhwFtoblvA1W7HYj+4KawcxQhgGyT0Vo5mBINkgSJ/9NB1hkDAiw0XJAVFaiyhdffk6wkDZ7oCBckGg2JbGh1uKs2b2drT0wvXAOGcbsYPGwXXWfDJbxJZPP4uSqK4ryiuZTYNKU4JhK4VFRSChkc/D52rbOhUW6e0uQ7pAwNOeZ1sLbMp2yZLKk8ptRPMjoNMc4aqj/HaBowNIxzs8C7cpwE2ckdLlLgm5uNPbMH5kvaLnDIYenmrPj9sQPuLUODIH3wzCNxVxFtdz/9llrGcexiEvtibkOiNwfpTS7KjpTVtsD085mQd+uqaBPE/slmRilm29hPyH+PzBurIcuf232LauCFH7S5XwxvpZpuQQVDKlyaPfMlNsy60AjK2mmYJrHJnLFA9kip8+ZfsP+WHdfe8+E856/kk/EOqsApOGECJS48gchGqcK2GYUm4Sw8vss7hpoT5GVDlyvM6wg6NhtdGyLQ9ZLAi4G2WF+kHMK+7qULK1gr4VBHTPkkAv6nrJt7b70iFGir1Kj/K4iC6vsWPPUGMHjgzmCxxiq/mS0jQVCfNGvvyvZOk1VxQdQFcWmlbowNRtRQfsMacc0XWNpikHHL2RcgIG/7V0mJxJWyYlFA306lSk5Rv5Jg94oq+mM66egDSqW31xSm16J9OmGTOrcWSwSEF5xMi43xGSA1FL0rTd6NQSODKIJNRvfmfJxodQvmPJGlfZoN2nZo2gEHMZorWDYJQ6UxkR1DsuRLXuN0xw2L8c2brXSGE4Ug+mW6vkHn6gdpqKIbpw7RDcVcc6JtpolGv11I1g3HAcQ+MGcGQQwBOKyBnaNU/E0XhROY4zvn2fGrfKqUZ1wrDK7TSWTXCNI4NJBWWTXOYejb6tiF7fU4jbVIHQpxDgyCB6UF/IZ4Xete3x9GK3aSnXxW3X7kzcPvHrfzdi5SAypVuVKV3itqros1EzhykyxByAoz6FylOvNbx7obI3XqANbNPG70nMahwZrFBQOBizUjkUSZjqM3VTkgAcGYQSihuXoZR5fQobBAobF6KU9RsmqCJcjlLWb6TguD6YUqaSe3h27plSyrzulDJS9ypB70qZeupGwHc9U0oZcGQQwPqf3dsoZflxFy6UkTZlwrBQ5pkSyoAjgzkFf7ovhLLbb1+/3XWfDGfVCnzubGyYCiPLlGAGPRmEESovZcXMCJAX2pqRZUo5Q1Z30hmpW4DRjXSWdYVDLzgcNcu64gVqaSrZRsotEDIlpkFPfapppH6VyftT03ojD/qqvebLjmZ1ngyWLSjCjFlPG4xEIFOCGvRkDky1TPHEy3+iSooiia2TPOLXeRVw5kqeVWoauKtXAW2oSY1U4LQ1noQ9G4SpuwXsGIRptAqnM2ScoPwzZolz0FBBouMvRTvwOT3WQJ2GywJZEHAzHLrgzIpB54wZ2a0Ys32iOaoHaQDGfHyd+rjQXWld7ZfMqwbaQb+E5Kc6s0mVzeDANsR6LNIy1fCJVDt3CUYXw5lWWWyvYaoRp85Tn8OZA8nbH39+WLCAts2YrtZTnVtuWg9Wem1pysXJTAPcsc8DvAmckPyNHM5z9ZbWo5UOgtvw+UWkzpNBOCFJ/ZKvzv7lJiqtPx8LV3l1lXpNp+VIJTaLv/mWo1b8XT3y8T8=</diagram></mxfile>

View File

@@ -12,7 +12,7 @@ setup(
url='https://home-assistant.io/', url='https://home-assistant.io/',
description=('Open-source private cloud os for Home-Assistant' description=('Open-source private cloud os for Home-Assistant'
' based on ResinOS'), ' based on ResinOS'),
long_description=('A maintenainless private cloud operator system that' long_description=('A maintainless private cloud operator system that'
'setup a Home-Assistant instance. Based on ResinOS'), 'setup a Home-Assistant instance. Based on ResinOS'),
classifiers=[ classifiers=[
'Intended Audience :: End Users/Desktop', 'Intended Audience :: End Users/Desktop',
@@ -29,7 +29,13 @@ setup(
keywords=['docker', 'home-assistant', 'api'], keywords=['docker', 'home-assistant', 'api'],
zip_safe=False, zip_safe=False,
platforms='any', platforms='any',
packages=['hassio', 'hassio.dock', 'hassio.api', 'hassio.addons'], packages=[
'hassio',
'hassio.dock',
'hassio.api',
'hassio.addons',
'hassio.snapshots'
],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'async_timeout', 'async_timeout',
@@ -38,5 +44,9 @@ setup(
'colorlog', 'colorlog',
'voluptuous', 'voluptuous',
'gitpython', 'gitpython',
'pyotp',
'pyqrcode',
'pytz',
'pyudev'
] ]
) )

View File

@@ -1,7 +1,8 @@
{ {
"hassio": "0.22", "hassio": "0.75",
"homeassistant": "0.44.1", "homeassistant": "0.57.3",
"resinos": "0.7", "resinos": "1.1",
"resinhup": "0.1", "resinhup": "0.3",
"generic": "0.3" "generic": "0.3",
"cluster": "0.1"
} }